diff --git a/src/plugins/discover/kibana.jsonc b/src/plugins/discover/kibana.jsonc
index 308e6cda10036b..2a99c4b1ff7f29 100644
--- a/src/plugins/discover/kibana.jsonc
+++ b/src/plugins/discover/kibana.jsonc
@@ -38,7 +38,8 @@
"savedObjectsTaggingOss",
"lens",
"noDataPage",
- "globalSearch"
+ "globalSearch",
+ "serverless"
],
"requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch"],
"extraPublicDirs": ["common"]
diff --git a/src/plugins/discover/public/__mocks__/discover_state.mock.ts b/src/plugins/discover/public/__mocks__/discover_state.mock.ts
index 3e0a36de1d595e..60e790cd752181 100644
--- a/src/plugins/discover/public/__mocks__/discover_state.mock.ts
+++ b/src/plugins/discover/public/__mocks__/discover_state.mock.ts
@@ -23,6 +23,10 @@ export function getDiscoverStateMock({
const container = getDiscoverStateContainer({
services: discoverServiceMock,
history,
+ customizationContext: {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+ },
});
container.savedSearchState.set(
savedSearch ? savedSearch : isTimeBased ? savedSearchMockWithTimeField : savedSearchMock
diff --git a/src/plugins/discover/public/application/discover_router.test.tsx b/src/plugins/discover/public/application/discover_router.test.tsx
index d151963ce921a1..3d58d065129697 100644
--- a/src/plugins/discover/public/application/discover_router.test.tsx
+++ b/src/plugins/discover/public/application/discover_router.test.tsx
@@ -11,13 +11,19 @@ import { Redirect, RouteProps } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import { createSearchSessionMock } from '../__mocks__/search_session';
import { discoverServiceMock as mockDiscoverServices } from '../__mocks__/services';
-import { CustomDiscoverRoutes, DiscoverRouter, DiscoverRoutes } from './discover_router';
+import {
+ CustomDiscoverRoutes,
+ DiscoverRouter,
+ DiscoverRoutes,
+ DiscoverRoutesProps,
+} from './discover_router';
import { DiscoverMainRoute } from './main';
import { SingleDocRoute } from './doc';
import { ContextAppRoute } from './context';
import { createProfileRegistry } from '../customizations/profile_registry';
import { addProfile } from '../../common/customizations';
import { NotFoundRoute } from './not_found';
+import type { DiscoverCustomizationContext } from './types';
let mockProfile: string | undefined;
@@ -43,9 +49,15 @@ const gatherRoutes = (wrapper: ShallowWrapper) => {
});
};
-const props = {
+const customizationContext: DiscoverCustomizationContext = {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+};
+
+const props: DiscoverRoutesProps = {
isDev: false,
customizationCallbacks: [],
+ customizationContext,
};
describe('DiscoverRoutes', () => {
@@ -147,12 +159,17 @@ describe('CustomDiscoverRoutes', () => {
it('should show DiscoverRoutes for a valid profile', () => {
mockProfile = 'test';
const component = shallow(
-
+
);
expect(component.find(DiscoverRoutes).getElement()).toMatchObject(
);
@@ -161,7 +178,11 @@ describe('CustomDiscoverRoutes', () => {
it('should show NotFoundRoute for an invalid profile', () => {
mockProfile = 'invalid';
const component = shallow(
-
+
);
expect(component.find(NotFoundRoute).getElement()).toMatchObject();
});
@@ -178,6 +199,7 @@ describe('DiscoverRouter', () => {
services={mockDiscoverServices}
history={history}
profileRegistry={profileRegistry}
+ customizationContext={customizationContext}
isDev={props.isDev}
/>
);
@@ -186,13 +208,21 @@ describe('DiscoverRouter', () => {
it('should show DiscoverRoutes component for / route', () => {
expect(pathMap['/']).toMatchObject(
-
+
);
});
it(`should show CustomDiscoverRoutes component for ${profilePath} route`, () => {
expect(pathMap[profilePath]).toMatchObject(
-
+
);
});
});
diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx
index 73c99d50b6fa69..5e146a768baa11 100644
--- a/src/plugins/discover/public/application/discover_router.tsx
+++ b/src/plugins/discover/public/application/discover_router.tsx
@@ -21,10 +21,12 @@ import { ViewAlertRoute } from './view_alert';
import type { CustomizationCallback } from '../customizations';
import type { DiscoverProfileRegistry } from '../customizations/profile_registry';
import { addProfile } from '../../common/customizations';
+import type { DiscoverCustomizationContext } from './types';
-interface DiscoverRoutesProps {
+export interface DiscoverRoutesProps {
prefix?: string;
customizationCallbacks: CustomizationCallback[];
+ customizationContext: DiscoverCustomizationContext;
isDev: boolean;
}
@@ -66,6 +68,7 @@ export const DiscoverRoutes = ({ prefix, ...mainRouteProps }: DiscoverRoutesProp
interface CustomDiscoverRoutesProps {
profileRegistry: DiscoverProfileRegistry;
+ customizationContext: DiscoverCustomizationContext;
isDev: boolean;
}
@@ -92,6 +95,7 @@ export const CustomDiscoverRoutes = ({ profileRegistry, ...props }: CustomDiscov
export interface DiscoverRouterProps {
services: DiscoverServices;
profileRegistry: DiscoverProfileRegistry;
+ customizationContext: DiscoverCustomizationContext;
history: History;
isDev: boolean;
}
diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx
index a9571e08e21ad0..2e26b32973210f 100644
--- a/src/plugins/discover/public/application/index.tsx
+++ b/src/plugins/discover/public/application/index.tsx
@@ -12,15 +12,23 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { DiscoverRouter } from './discover_router';
import { DiscoverServices } from '../build_services';
import type { DiscoverProfileRegistry } from '../customizations/profile_registry';
+import type { DiscoverCustomizationContext } from './types';
export interface RenderAppProps {
element: HTMLElement;
services: DiscoverServices;
profileRegistry: DiscoverProfileRegistry;
+ customizationContext: DiscoverCustomizationContext;
isDev: boolean;
}
-export const renderApp = ({ element, services, profileRegistry, isDev }: RenderAppProps) => {
+export const renderApp = ({
+ element,
+ services,
+ profileRegistry,
+ customizationContext,
+ isDev,
+}: RenderAppProps) => {
const { history: getHistory, capabilities, chrome, data, core } = services;
const history = getHistory();
@@ -39,6 +47,7 @@ export const renderApp = ({ element, services, profileRegistry, isDev }: RenderA
,
diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
index 5a5c2bb0396835..54d2dc5da0084d 100644
--- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
+++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
@@ -15,7 +15,7 @@ import { createHashHistory } from 'history';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { buildDataTableRecordList } from '@kbn/discover-utils';
import { esHitsMock } from '@kbn/discover-utils/src/__mocks__';
-import { FetchStatus } from '../../../../types';
+import { DiscoverCustomizationContext, FetchStatus } from '../../../../types';
import {
AvailableFields$,
DataDocuments$,
@@ -124,11 +124,17 @@ function getSavedSearch(dataView: DataView) {
} as unknown as SavedSearch;
}
+const customizationContext: DiscoverCustomizationContext = {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+};
+
export function getDocumentsLayoutProps(dataView: DataView) {
const stateContainer = getDiscoverStateContainer({
history: createHashHistory(),
savedSearch: getSavedSearch(dataView),
services,
+ customizationContext,
});
stateContainer.appState.set({
columns: ['name', 'message', 'bytes'],
@@ -153,6 +159,7 @@ export const getPlainRecordLayoutProps = (dataView: DataView) => {
history: createHashHistory(),
savedSearch: getSavedSearch(dataView),
services,
+ customizationContext,
});
stateContainer.appState.set({
columns: ['name', 'message', 'bytes'],
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss
index 55972b4f7f6291..352a1f803bc352 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss
@@ -11,6 +11,10 @@ discover-app {
.dscPage {
@include euiBreakpoint('m', 'l', 'xl') {
@include kibanaFullBodyHeight();
+
+ &.dscPage--serverless {
+ @include kibanaFullBodyHeight($euiSize * 3);
+ }
}
flex-direction: column;
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
index b2d3a4b5058ee9..622ef340ffa984 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
@@ -37,7 +37,6 @@ import { DiscoverStateContainer } from '../../services/discover_state';
import { VIEW_MODE } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../services/discover_internal_state_container';
import { useAppStateSelector } from '../../services/discover_app_state_container';
-import { useInspector } from '../../hooks/use_inspector';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverNoResults } from '../no_results';
import { LoadingSpinner } from '../loading_spinner/loading_spinner';
@@ -73,8 +72,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
filterManager,
history,
spaces,
- inspector,
docLinks,
+ serverless,
} = useDiscoverServices();
const { euiTheme } = useEuiTheme();
const pageBackgroundColor = useEuiBackgroundColor('plain');
@@ -118,11 +117,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
[dataState.fetchStatus, dataState.foundDocuments]
);
- const onOpenInspector = useInspector({
- inspector,
- stateContainer,
- });
-
const {
columns: currentColumns,
onAddColumn,
@@ -243,7 +237,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
return (
({
...jest.requireActual('@kbn/kibana-react-plugin/public'),
- useKibana: () => ({
- services: mockDiscoverService,
- }),
+ useKibana: jest.fn(),
}));
const MockCustomSearchBar: typeof mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu =
@@ -77,15 +75,14 @@ function getProps(
return {
stateContainer,
- query: {} as Query,
savedQuery: '',
updateQuery: jest.fn(),
- onOpenInspector: jest.fn(),
onFieldEdited: jest.fn(),
- isPlainRecord: false,
};
}
+const mockUseKibana = useKibana as jest.Mock;
+
describe('Discover topnav component', () => {
beforeEach(() => {
mockTopNavCustomization.defaultMenu = undefined;
@@ -107,6 +104,10 @@ describe('Discover topnav component', () => {
throw new Error(`Unknown customization id: ${id}`);
}
});
+
+ mockUseKibana.mockReturnValue({
+ services: mockDiscoverService,
+ });
});
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
@@ -280,4 +281,38 @@ describe('Discover topnav component', () => {
expect(topNav.prop('dataViewPickerComponentProps')).toBeUndefined();
});
});
+
+ describe('serverless', () => {
+ it('should render top nav when serverless plugin is not defined', () => {
+ const props = getProps();
+ const component = mountWithIntl(
+
+
+
+ );
+ const searchBar = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu);
+ expect(searchBar.prop('badges')).toBeDefined();
+ expect(searchBar.prop('config')).toBeDefined();
+ expect(searchBar.prop('setMenuMountPoint')).toBeDefined();
+ });
+
+ it('should not render top nav when serverless plugin is defined', () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ const component = mountWithIntl(
+
+
+
+ );
+ const searchBar = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu);
+ expect(searchBar.prop('badges')).toBeUndefined();
+ expect(searchBar.prop('config')).toBeUndefined();
+ expect(searchBar.prop('setMenuMountPoint')).toBeUndefined();
+ });
+ });
});
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
index 51484519ee7ac3..3e8d58297783cd 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
@@ -5,8 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
-import useObservable from 'react-use/lib/useObservable';
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import { DataViewType, type DataView } from '@kbn/data-views-plugin/public';
import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
@@ -14,51 +14,48 @@ import { ENABLE_ESQL } from '@kbn/discover-utils';
import { useSavedSearchInitial } from '../../services/discover_state_provider';
import { useInternalStateSelector } from '../../services/discover_internal_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
-import { getTopNavLinks } from './get_top_nav_links';
-import { getTopNavBadges } from './get_top_nav_badges';
import { getHeaderActionMenuMounter } from '../../../../kibana_services';
import { DiscoverStateContainer } from '../../services/discover_state';
import { onSaveSearch } from './on_save_search';
import { useDiscoverCustomization } from '../../../../customizations';
import { addLog } from '../../../../utils/add_log';
+import { useAppStateSelector } from '../../services/discover_app_state_container';
+import { isTextBasedQuery } from '../../utils/is_text_based_query';
+import { useDiscoverTopNav } from './use_discover_topnav';
export interface DiscoverTopNavProps {
- onOpenInspector: () => void;
- query?: Query | AggregateQuery;
savedQuery?: string;
updateQuery: (
payload: { dateRange: TimeRange; query?: Query | AggregateQuery },
isUpdate?: boolean
) => void;
stateContainer: DiscoverStateContainer;
- isPlainRecord: boolean;
textBasedLanguageModeErrors?: Error;
textBasedLanguageModeWarning?: string;
onFieldEdited: () => Promise;
}
export const DiscoverTopNav = ({
- onOpenInspector,
- query,
savedQuery,
stateContainer,
updateQuery,
- isPlainRecord,
textBasedLanguageModeErrors,
textBasedLanguageModeWarning,
onFieldEdited,
}: DiscoverTopNavProps) => {
+ const query = useAppStateSelector((state) => state.query);
const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews);
const dataView = useInternalStateSelector((state) => state.dataView!);
const savedDataViews = useInternalStateSelector((state) => state.savedDataViews);
const savedSearch = useSavedSearchInitial();
+ const isTextBased = useMemo(() => isTextBasedQuery(query), [query]);
const showDatePicker = useMemo(() => {
// always show the timepicker for text based languages
return (
- isPlainRecord ||
- (!isPlainRecord && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP)
+ isTextBased ||
+ (!isTextBased && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP)
);
- }, [dataView, isPlainRecord]);
+ }, [dataView, isTextBased]);
const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data, uiSettings, dataViews } = services;
@@ -115,44 +112,6 @@ export const DiscoverTopNav = ({
});
}, [dataViewEditor, stateContainer]);
- const topNavCustomization = useDiscoverCustomization('top_nav');
-
- const hasSavedSearchChanges = useObservable(stateContainer.savedSearchState.getHasChanged$());
- const hasUnsavedChanges =
- hasSavedSearchChanges && Boolean(stateContainer.savedSearchState.getId());
- const topNavBadges = useMemo(
- () =>
- getTopNavBadges({
- stateContainer,
- services,
- hasUnsavedChanges,
- topNavCustomization,
- }),
- [stateContainer, services, hasUnsavedChanges, topNavCustomization]
- );
-
- const topNavMenu = useMemo(
- () =>
- getTopNavLinks({
- dataView,
- services,
- state: stateContainer,
- onOpenInspector,
- isPlainRecord,
- adHocDataViews,
- topNavCustomization,
- }),
- [
- adHocDataViews,
- dataView,
- isPlainRecord,
- onOpenInspector,
- services,
- stateContainer,
- topNavCustomization,
- ]
- );
-
const onEditDataView = async (editedDataView: DataView) => {
if (editedDataView.isPersisted()) {
// Clear the current data view from the cache and create a new instance
@@ -229,16 +188,21 @@ export const DiscoverTopNav = ({
[services, stateContainer]
);
+ const { topNavBadges, topNavMenu } = useDiscoverTopNav({ stateContainer });
+ const topNavProps = !services.serverless && {
+ badges: topNavBadges,
+ config: topNavMenu,
+ setMenuMountPoint,
+ };
+
return (
({
+ ...jest.requireActual('@kbn/kibana-react-plugin/public'),
+ useKibana: jest.fn(),
+}));
+
+function getProps({ hideNavMenuItems }: { hideNavMenuItems?: boolean } = {}) {
+ const stateContainer = getDiscoverStateMock({ isTimeBased: true });
+ stateContainer.internalState.transitions.setDataView(dataViewMock);
+
+ return {
+ stateContainer,
+ hideNavMenuItems,
+ };
+}
+
+const mockUseKibana = useKibana as jest.Mock;
+
+describe('DiscoverTopNavServerless', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseKibana.mockReturnValue({
+ services: mockDiscoverService,
+ });
+ });
+
+ it('should not render when serverless plugin is not defined', async () => {
+ const props = getProps();
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).toBeNull();
+ });
+
+ it('should render when serverless plugin is defined and displayMode is "standalone"', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).not.toBeNull();
+ });
+
+ it('should not render when serverless plugin is defined and displayMode is not "standalone"', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ props.stateContainer.customizationContext.displayMode = 'embedded';
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).toBeNull();
+ });
+
+ describe('nav menu items', () => {
+ it('should show nav menu items when hideNavMenuItems is false', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).not.toBeNull();
+ await waitFor(() => {
+ const topNavMenuItems = screen.queryByTestId('topNavMenuItems');
+ expect(topNavMenuItems).not.toBeNull();
+ });
+ });
+
+ it('should hide nav menu items when hideNavMenuItems is true', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps({ hideNavMenuItems: true });
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).not.toBeNull();
+ await waitFor(() => {
+ const topNavMenuItems = screen.queryByTestId('topNavMenuItems');
+ expect(topNavMenuItems).toBeNull();
+ });
+ });
+ });
+
+ describe('LogExplorerTabs', () => {
+ it('should render when showLogExplorerTabs is true', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ props.stateContainer.customizationContext.showLogExplorerTabs = true;
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).not.toBeNull();
+ await waitFor(() => {
+ const logExplorerTabs = screen.queryByTestId('logExplorerTabs');
+ expect(logExplorerTabs).not.toBeNull();
+ });
+ });
+
+ it('should not render when showLogExplorerTabs is false', async () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ ...mockDiscoverService,
+ serverless: true,
+ },
+ });
+ const props = getProps();
+ render(
+
+
+
+ );
+ const topNav = screen.queryByTestId('discoverTopNavServerless');
+ expect(topNav).not.toBeNull();
+ await waitFor(() => {
+ const logExplorerTabs = screen.queryByTestId('logExplorerTabs');
+ expect(logExplorerTabs).toBeNull();
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav_serverless.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav_serverless.tsx
new file mode 100644
index 00000000000000..800ceeae760132
--- /dev/null
+++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav_serverless.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '@elastic/eui';
+import { TopNavMenuBadges, TopNavMenuItems } from '@kbn/navigation-plugin/public';
+import { LogExplorerTabs } from '../../../../components/log_explorer_tabs';
+import { useDiscoverServices } from '../../../../hooks/use_discover_services';
+import { useDiscoverTopNav } from './use_discover_topnav';
+import type { DiscoverStateContainer } from '../../services/discover_state';
+
+export const DiscoverTopNavServerless = ({
+ stateContainer,
+ hideNavMenuItems,
+}: {
+ stateContainer: DiscoverStateContainer;
+ hideNavMenuItems?: boolean;
+}) => {
+ const { customizationContext } = stateContainer;
+ const services = useDiscoverServices();
+ const { topNavBadges, topNavMenu } = useDiscoverTopNav({ stateContainer });
+
+ if (!services.serverless || customizationContext.displayMode !== 'standalone') {
+ return null;
+ }
+
+ return (
+
+ {customizationContext.showLogExplorerTabs && (
+
+
+
+
+
+ )}
+ {!hideNavMenuItems && (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
index 4e3e7068f883d0..e83f517242e373 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
+++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
@@ -27,7 +27,7 @@ test('getTopNavLinks result', () => {
onOpenInspector: jest.fn(),
services,
state,
- isPlainRecord: false,
+ isTextBased: false,
adHocDataViews: [],
topNavCustomization: undefined,
});
@@ -80,7 +80,7 @@ test('getTopNavLinks result for ES|QL mode', () => {
onOpenInspector: jest.fn(),
services,
state,
- isPlainRecord: true,
+ isTextBased: true,
adHocDataViews: [],
topNavCustomization: undefined,
});
diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
index ef2a6fb9ad28b7..0f066760b9e2eb 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
@@ -27,15 +27,15 @@ export const getTopNavLinks = ({
services,
state,
onOpenInspector,
- isPlainRecord,
+ isTextBased,
adHocDataViews,
topNavCustomization,
}: {
- dataView: DataView;
+ dataView: DataView | undefined;
services: DiscoverServices;
state: DiscoverStateContainer;
onOpenInspector: () => void;
- isPlainRecord: boolean;
+ isTextBased: boolean;
adHocDataViews: DataView[];
topNavCustomization: TopNavCustomization | undefined;
}): TopNavMenuData[] => {
@@ -53,7 +53,7 @@ export const getTopNavLinks = ({
services,
stateContainer: state,
adHocDataViews,
- isPlainRecord,
+ isPlainRecord: isTextBased,
});
},
testId: 'discoverAlertsButton',
@@ -126,7 +126,7 @@ export const getTopNavLinks = ({
savedSearch.searchSource,
state.appState.getState(),
services,
- isPlainRecord
+ isTextBased
);
const { locator } = services;
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
index 5506010528e05c..1d422baf238fff 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
@@ -25,6 +25,10 @@ function getStateContainer({ dataView }: { dataView?: DataView } = {}) {
const stateContainer = getDiscoverStateContainer({
services: discoverServiceMock,
history,
+ customizationContext: {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+ },
});
stateContainer.savedSearchState.set(savedSearch);
stateContainer.appState.getState = jest.fn(() => ({
diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts
new file mode 100644
index 00000000000000..7304fd941c4a38
--- /dev/null
+++ b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useMemo } from 'react';
+import useObservable from 'react-use/lib/useObservable';
+import { useDiscoverCustomization } from '../../../../customizations';
+import { useDiscoverServices } from '../../../../hooks/use_discover_services';
+import { useInspector } from '../../hooks/use_inspector';
+import { useAppStateSelector } from '../../services/discover_app_state_container';
+import { useInternalStateSelector } from '../../services/discover_internal_state_container';
+import type { DiscoverStateContainer } from '../../services/discover_state';
+import { isTextBasedQuery } from '../../utils/is_text_based_query';
+import { getTopNavBadges } from './get_top_nav_badges';
+import { getTopNavLinks } from './get_top_nav_links';
+
+export const useDiscoverTopNav = ({
+ stateContainer,
+}: {
+ stateContainer: DiscoverStateContainer;
+}) => {
+ const services = useDiscoverServices();
+ const topNavCustomization = useDiscoverCustomization('top_nav');
+ const hasSavedSearchChanges = useObservable(stateContainer.savedSearchState.getHasChanged$());
+ const hasUnsavedChanges = Boolean(
+ hasSavedSearchChanges && stateContainer.savedSearchState.getId()
+ );
+
+ const topNavBadges = useMemo(
+ () =>
+ getTopNavBadges({
+ stateContainer,
+ services,
+ hasUnsavedChanges,
+ topNavCustomization,
+ }),
+ [stateContainer, services, hasUnsavedChanges, topNavCustomization]
+ );
+
+ const dataView = useInternalStateSelector((state) => state.dataView);
+ const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews);
+ const query = useAppStateSelector((state) => state.query);
+ const isTextBased = useMemo(() => isTextBasedQuery(query), [query]);
+ const onOpenInspector = useInspector({
+ inspector: services.inspector,
+ stateContainer,
+ });
+
+ const topNavMenu = useMemo(
+ () =>
+ getTopNavLinks({
+ dataView,
+ services,
+ state: stateContainer,
+ onOpenInspector,
+ isTextBased,
+ adHocDataViews,
+ topNavCustomization,
+ }),
+ [
+ adHocDataViews,
+ dataView,
+ isTextBased,
+ onOpenInspector,
+ services,
+ stateContainer,
+ topNavCustomization,
+ ]
+ );
+
+ return {
+ topNavMenu,
+ topNavBadges,
+ };
+};
diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx
index dbc1db449b0300..d10e74a66523b9 100644
--- a/src/plugins/discover/public/application/main/discover_main_app.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_app.tsx
@@ -18,7 +18,6 @@ import { useSavedSearchAliasMatchRedirect } from '../../hooks/saved_search_alias
import { useSavedSearchInitial } from './services/discover_state_provider';
import { useAdHocDataViews } from './hooks/use_adhoc_data_views';
import { useTextBasedQueryLanguage } from './hooks/use_text_based_query_language';
-import type { DiscoverDisplayMode } from '../types';
import { addLog } from '../../utils/add_log';
const DiscoverLayoutMemoized = React.memo(DiscoverLayout);
@@ -28,11 +27,10 @@ export interface DiscoverMainProps {
* Central state container
*/
stateContainer: DiscoverStateContainer;
- mode?: DiscoverDisplayMode;
}
export function DiscoverMainApp(props: DiscoverMainProps) {
- const { stateContainer, mode = 'standalone' } = props;
+ const { stateContainer } = props;
const savedSearch = useSavedSearchInitial();
const services = useDiscoverServices();
const { chrome, docLinks, data, spaces, history } = services;
@@ -65,12 +63,18 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
* SavedSearch dependent initializing
*/
useEffect(() => {
- if (mode === 'standalone') {
+ if (stateContainer.customizationContext.displayMode === 'standalone') {
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
setBreadcrumbs({ titleBreadcrumbText: savedSearch.title, services });
}
- }, [mode, chrome.docTitle, savedSearch.id, savedSearch.title, services]);
+ }, [
+ chrome.docTitle,
+ savedSearch.id,
+ savedSearch.title,
+ services,
+ stateContainer.customizationContext.displayMode,
+ ]);
useEffect(() => {
addHelpMenuToAppChrome(chrome, docLinks);
diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx
index 560f998b4bd94e..63296bfb927e93 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx
@@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
import { setHeaderActionMenuMounter, setScopedHistory } from '../../kibana_services';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { discoverServiceMock } from '../../__mocks__/services';
-import { DiscoverMainRoute } from './discover_main_route';
+import { DiscoverMainRoute, MainRouteProps } from './discover_main_route';
import { MemoryRouter } from 'react-router-dom';
import { DiscoverMainApp } from './discover_main_app';
import { findTestSubject } from '@elastic/eui/lib/test';
@@ -20,6 +20,7 @@ import {
createCustomizationService,
DiscoverCustomizationService,
} from '../../customizations/customization_service';
+import { DiscoverTopNavServerless } from './components/top_nav/discover_topnav_serverless';
let mockCustomizationService: DiscoverCustomizationService | undefined;
@@ -98,12 +99,26 @@ describe('DiscoverMainRoute', () => {
expect(component.find(DiscoverMainApp).exists()).toBe(true);
});
});
+
+ test('should pass hideNavMenuItems=true to DiscoverTopNavServerless while loading', async () => {
+ const component = mountComponent(true, true);
+ expect(component.find(DiscoverTopNavServerless).prop('hideNavMenuItems')).toBe(true);
+ await waitFor(() => {
+ expect(component.update().find(DiscoverTopNavServerless).prop('hideNavMenuItems')).toBe(
+ false
+ );
+ });
+ });
});
const mountComponent = (hasESData = true, hasUserDataView = true) => {
- const props = {
+ const props: MainRouteProps = {
isDev: false,
customizationCallbacks: [],
+ customizationContext: {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+ },
};
return mountWithIntl(
diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx
index 9e46f2134acd8c..1bb24661da0cbf 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.tsx
@@ -5,6 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+
import React, { useEffect, useState, memo, useCallback, useMemo } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import type { DataView } from '@kbn/data-views-plugin/public';
@@ -34,7 +35,8 @@ import {
DiscoverCustomizationProvider,
useDiscoverCustomizationService,
} from '../../customizations';
-import type { DiscoverDisplayMode } from '../types';
+import type { DiscoverCustomizationContext } from '../types';
+import { DiscoverTopNavServerless } from './components/top_nav/discover_topnav_serverless';
const DiscoverMainAppMemoized = memo(DiscoverMainApp);
@@ -45,10 +47,13 @@ interface DiscoverLandingParams {
export interface MainRouteProps {
customizationCallbacks: CustomizationCallback[];
isDev: boolean;
- mode?: DiscoverDisplayMode;
+ customizationContext: DiscoverCustomizationContext;
}
-export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone' }: MainRouteProps) {
+export function DiscoverMainRoute({
+ customizationCallbacks,
+ customizationContext,
+}: MainRouteProps) {
const history = useHistory();
const services = useDiscoverServices();
const {
@@ -60,12 +65,11 @@ export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone'
dataViewEditor,
} = services;
const { id: savedSearchId } = useParams();
-
const stateContainer = useSingleton(() =>
getDiscoverStateContainer({
history,
services,
- mode,
+ customizationContext,
})
);
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
@@ -150,7 +154,7 @@ export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone'
dataView: nextDataView,
dataViewSpec: historyLocationState?.dataViewSpec,
});
- if (mode === 'standalone') {
+ if (customizationContext.displayMode === 'standalone') {
if (currentSavedSearch?.id) {
chrome.recentlyAccessed.add(
getSavedSearchFullPathUrl(currentSavedSearch.id),
@@ -195,32 +199,20 @@ export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone'
},
[
checkData,
- stateContainer,
+ stateContainer.actions,
savedSearchId,
historyLocationState?.dataViewSpec,
- chrome,
+ customizationContext.displayMode,
services,
+ chrome.recentlyAccessed,
history,
core.application.navigateToApp,
core.theme,
basePath,
toastNotifications,
- mode,
]
);
- const onDataViewCreated = useCallback(
- async (nextDataView: unknown) => {
- if (nextDataView) {
- setLoading(true);
- setShowNoDataPage(false);
- setError(undefined);
- await loadSavedSearch(nextDataView as DataView);
- }
- },
- [loadSavedSearch]
- );
-
useEffect(() => {
if (!isCustomizationServiceInitialized) return;
@@ -244,8 +236,20 @@ export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone'
},
});
- if (showNoDataPage) {
- const analyticsServices = {
+ const onDataViewCreated = useCallback(
+ async (nextDataView: unknown) => {
+ if (nextDataView) {
+ setLoading(true);
+ setShowNoDataPage(false);
+ setError(undefined);
+ await loadSavedSearch(nextDataView as DataView);
+ }
+ },
+ [loadSavedSearch]
+ );
+
+ const noDataDependencies = useMemo(
+ () => ({
coreStart: core,
dataViews: {
...data.dataViews,
@@ -260,27 +264,53 @@ export function DiscoverMainRoute({ customizationCallbacks, mode = 'standalone'
},
dataViewEditor,
noDataPage: services.noDataPage,
- };
+ }),
+ [core, data.dataViews, dataViewEditor, hasESData, hasUserDataView, services.noDataPage]
+ );
- return (
-
-
-
- );
- }
+ const loadingIndicator = useMemo(
+ () => ,
+ [hasCustomBranding]
+ );
+
+ const mainContent = useMemo(() => {
+ if (showNoDataPage) {
+ return (
+
+
+
+ );
+ }
+
+ if (loading) {
+ return loadingIndicator;
+ }
+
+ return ;
+ }, [
+ loading,
+ loadingIndicator,
+ noDataDependencies,
+ onDataViewCreated,
+ showNoDataPage,
+ stateContainer,
+ ]);
if (error) {
return ;
}
- if (loading || !customizationService) {
- return ;
+ if (!customizationService) {
+ return loadingIndicator;
}
return (
-
+ <>
+
+ {mainContent}
+ >
);
diff --git a/src/plugins/discover/public/application/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts
index c946ac70bbe62f..a14a7d3b797c67 100644
--- a/src/plugins/discover/public/application/main/services/discover_state.test.ts
+++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts
@@ -22,9 +22,9 @@ import {
} from '../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../__mocks__/services';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
-import { DiscoverAppStateContainer } from './discover_app_state_container';
+import type { DiscoverAppStateContainer } from './discover_app_state_container';
import { waitFor } from '@testing-library/react';
-import { FetchStatus } from '../../types';
+import { DiscoverCustomizationContext, FetchStatus } from '../../types';
import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex';
import { copySavedSearch } from './discover_saved_search_container';
@@ -34,6 +34,11 @@ const startSync = (appState: DiscoverAppStateContainer) => {
return stop;
};
+const customizationContext: DiscoverCustomizationContext = {
+ displayMode: 'standalone',
+ showLogExplorerTabs: false,
+};
+
async function getState(
url: string = '/',
{ savedSearch, isEmptyUrl }: { savedSearch?: SavedSearch; isEmptyUrl?: boolean } = {}
@@ -51,6 +56,7 @@ async function getState(
const nextState = getDiscoverStateContainer({
services: discoverServiceMock,
history: nextHistory,
+ customizationContext,
});
nextState.appState.isEmptyURL = jest.fn(() => isEmptyUrl ?? true);
jest.spyOn(nextState.dataState, 'fetch');
@@ -87,9 +93,10 @@ describe('Test discover state', () => {
state = getDiscoverStateContainer({
services: discoverServiceMock,
history,
+ customizationContext,
});
state.savedSearchState.set(savedSearchMock);
- await state.appState.update({}, true);
+ state.appState.update({}, true);
stopSync = startSync(state.appState);
});
afterEach(() => {
@@ -137,7 +144,10 @@ describe('Test discover state', () => {
test('pauseAutoRefreshInterval sets refreshInterval.pause to true', async () => {
history.push('/#?_g=(refreshInterval:(pause:!f,value:5000))');
expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!f,value:5000))');
- await state.actions.setDataView(dataViewMock);
+ // TODO: state.actions.setDataView should be async because it calls pauseAutoRefreshInterval which is async.
+ // I found this bug while removing unnecessary awaits, but it will need to be fixed in a follow up PR.
+ state.actions.setDataView(dataViewMock);
+ await new Promise(process.nextTick);
expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!t,value:5000))');
});
});
@@ -190,6 +200,7 @@ describe('Test createSearchSessionRestorationDataProvider', () => {
const discoverStateContainer = getDiscoverStateContainer({
services: discoverServiceMock,
history,
+ customizationContext,
});
discoverStateContainer.appState.update({
index: savedSearchMock.searchSource.getField('index')!.id,
@@ -661,9 +672,9 @@ describe('Test discover state actions', () => {
const { state } = await getState('/', { savedSearch: savedSearchMock });
const unsubscribe = state.actions.initializeAndSync();
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
- await state.savedSearchState.update({ nextState: { hideChart: true } });
+ state.savedSearchState.update({ nextState: { hideChart: true } });
expect(state.savedSearchState.getState().hideChart).toBe(true);
- await state.actions.onOpenSavedSearch(savedSearchMock.id!);
+ state.actions.onOpenSavedSearch(savedSearchMock.id!);
expect(state.savedSearchState.getState().hideChart).toBe(undefined);
unsubscribe();
});
@@ -756,16 +767,21 @@ describe('Test discover state with embedded mode', () => {
state = getDiscoverStateContainer({
services: discoverServiceMock,
history,
- mode: 'embedded',
+ customizationContext: {
+ ...customizationContext,
+ displayMode: 'embedded',
+ },
});
state.savedSearchState.set(savedSearchMock);
- await state.appState.update({}, true);
+ state.appState.update({}, true);
stopSync = startSync(state.appState);
});
+
afterEach(() => {
stopSync();
stopSync = () => {};
});
+
test('setting app state and syncing to URL', async () => {
state.appState.update({ index: 'modified' });
await new Promise(process.nextTick);
diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts
index b3abfed5e6eccf..507076391c3846 100644
--- a/src/plugins/discover/public/application/main/services/discover_state.ts
+++ b/src/plugins/discover/public/application/main/services/discover_state.ts
@@ -26,7 +26,7 @@ import { merge } from 'rxjs';
import { AggregateQuery, Query, TimeRange } from '@kbn/es-query';
import { loadSavedSearch as loadSavedSearchFn } from './load_saved_search';
import { restoreStateFromSavedSearch } from '../../../services/saved_searches/restore_from_saved_search';
-import { DiscoverDisplayMode, FetchStatus } from '../../types';
+import { DiscoverCustomizationContext, FetchStatus } from '../../types';
import { changeDataView } from '../hooks/utils/change_data_view';
import { buildStateSubscribe } from '../hooks/utils/build_state_subscribe';
import { addLog } from '../../../utils/add_log';
@@ -67,11 +67,10 @@ interface DiscoverStateContainerParams {
* core ui settings service
*/
services: DiscoverServices;
- /*
- * mode in which discover is running
- *
- * */
- mode?: DiscoverDisplayMode;
+ /**
+ * Context object for customization related properties
+ */
+ customizationContext: DiscoverCustomizationContext;
}
export interface LoadParams {
@@ -94,7 +93,6 @@ export interface DiscoverStateContainer {
* Global State, the _g part of the URL
*/
globalState: DiscoverGlobalStateContainer;
-
/**
* App state, the _a part of the URL
*/
@@ -119,6 +117,10 @@ export interface DiscoverStateContainer {
* Service for handling search sessions
*/
searchSessionManager: DiscoverSearchSessionManager;
+ /**
+ * Context object for customization related properties
+ */
+ customizationContext: DiscoverCustomizationContext;
/**
* Complex functions to update multiple containers from UI
*/
@@ -190,7 +192,7 @@ export interface DiscoverStateContainer {
* When saving a saved search with an ad hoc data view, a new id needs to be generated for the data view
* This is to prevent duplicate ids messing with our system
*/
- updateAdHocDataViewId: () => void;
+ updateAdHocDataViewId: () => Promise;
};
}
@@ -201,7 +203,7 @@ export interface DiscoverStateContainer {
export function getDiscoverStateContainer({
history,
services,
- mode = 'standalone',
+ customizationContext,
}: DiscoverStateContainerParams): DiscoverStateContainer {
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
const toasts = services.core.notifications.toasts;
@@ -212,7 +214,7 @@ export function getDiscoverStateContainer({
const stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
- useHashQuery: mode !== 'embedded',
+ useHashQuery: customizationContext.displayMode !== 'embedded',
...(toasts && withNotifyOnErrors(toasts)),
});
@@ -475,6 +477,7 @@ export function getDiscoverStateContainer({
savedSearchState: savedSearchContainer,
stateStorage,
searchSessionManager,
+ customizationContext,
actions: {
initializeAndSync,
fetchData,
diff --git a/src/plugins/discover/public/application/types.ts b/src/plugins/discover/public/application/types.ts
index 7e3413143ae516..70773d2db521fa 100644
--- a/src/plugins/discover/public/application/types.ts
+++ b/src/plugins/discover/public/application/types.ts
@@ -21,6 +21,17 @@ export enum FetchStatus {
export type DiscoverDisplayMode = 'embedded' | 'standalone';
+export interface DiscoverCustomizationContext {
+ /*
+ * Display mode in which discover is running
+ */
+ displayMode: DiscoverDisplayMode;
+ /**
+ * Whether or not to show the Log Explorer tabs
+ */
+ showLogExplorerTabs: boolean;
+}
+
export interface RecordsFetchResponse {
records: DataTableRecord[];
textBasedQueryColumns?: DatatableColumn[];
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index da82df24e184f4..8c69c53e317e3d 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -53,6 +53,7 @@ import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { ContentClient } from '@kbn/content-management-plugin/public';
import { memoize } from 'lodash';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
+import type { ServerlessPluginStart } from '@kbn/serverless/public';
import { getHistory } from './kibana_services';
import { DiscoverStartPlugins } from './plugin';
import { DiscoverContextAppLocator } from './application/context/services/locator';
@@ -111,6 +112,7 @@ export interface DiscoverServices {
uiActions: UiActionsStart;
contentClient: ContentClient;
noDataPage?: NoDataPagePluginStart;
+ serverless?: ServerlessPluginStart;
}
export const buildServices = memoize(function (
@@ -171,5 +173,6 @@ export const buildServices = memoize(function (
uiActions: plugins.uiActions,
contentClient: plugins.contentManagement.client,
noDataPage: plugins.noDataPage,
+ serverless: plugins.serverless,
};
});
diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx
index 8c879fdfef7bc6..43426c83c77305 100644
--- a/src/plugins/discover/public/components/discover_container/discover_container.tsx
+++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx
@@ -16,6 +16,7 @@ import type { DiscoverServices } from '../../build_services';
import type { CustomizationCallback } from '../../customizations';
import { setHeaderActionMenuMounter, setScopedHistory } from '../../kibana_services';
import { LoadingIndicator } from '../common/loading_indicator';
+import type { DiscoverCustomizationContext } from '../../application/types';
export interface DiscoverContainerInternalProps {
/*
@@ -43,6 +44,11 @@ const discoverContainerWrapperCss = css`
}
`;
+const customizationContext: DiscoverCustomizationContext = {
+ displayMode: 'embedded',
+ showLogExplorerTabs: false,
+};
+
export const DiscoverContainerInternal = ({
overrideServices,
scopedHistory,
@@ -90,7 +96,7 @@ export const DiscoverContainerInternal = ({
diff --git a/src/plugins/discover/public/components/log_explorer_tabs/index.ts b/src/plugins/discover/public/components/log_explorer_tabs/index.ts
new file mode 100644
index 00000000000000..b144b8fec104f0
--- /dev/null
+++ b/src/plugins/discover/public/components/log_explorer_tabs/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { withSuspense } from '@kbn/shared-ux-utility';
+import { lazy } from 'react';
+
+export type { LogExplorerTabsProps } from './log_explorer_tabs';
+
+export const LogExplorerTabs = withSuspense(lazy(() => import('./log_explorer_tabs')));
diff --git a/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.test.tsx b/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.test.tsx
new file mode 100644
index 00000000000000..5e4d295a7c89fa
--- /dev/null
+++ b/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { discoverServiceMock } from '../../__mocks__/services';
+import { LogExplorerTabs, LogExplorerTabsProps } from './log_explorer_tabs';
+import { DISCOVER_APP_LOCATOR } from '../../../common';
+import { ALL_DATASETS_LOCATOR_ID } from '@kbn/deeplinks-observability';
+
+const createMockLocator = (id: string) => ({
+ navigate: jest.fn(),
+ getRedirectUrl: jest.fn().mockReturnValue(id),
+});
+
+describe('LogExplorerTabs', () => {
+ const renderTabs = (selectedTab: LogExplorerTabsProps['selectedTab'] = 'discover') => {
+ const mockDiscoverLocator = createMockLocator(DISCOVER_APP_LOCATOR);
+ const mockLogExplorerLocator = createMockLocator(ALL_DATASETS_LOCATOR_ID);
+ const services = {
+ ...discoverServiceMock,
+ share: {
+ ...discoverServiceMock.share,
+ url: {
+ ...discoverServiceMock.share?.url,
+ locators: {
+ get: jest.fn((id) => {
+ switch (id) {
+ case DISCOVER_APP_LOCATOR:
+ return mockDiscoverLocator;
+ case ALL_DATASETS_LOCATOR_ID:
+ return mockLogExplorerLocator;
+ default:
+ throw new Error(`Unknown locator id: ${id}`);
+ }
+ }),
+ },
+ },
+ },
+ } as unknown as typeof discoverServiceMock;
+
+ render();
+
+ return {
+ mockDiscoverLocator,
+ mockLogExplorerLocator,
+ };
+ };
+
+ const getDiscoverTab = () => screen.getByText('Discover').closest('a')!;
+ const getLogExplorerTab = () => screen.getByText('Logs Explorer').closest('a')!;
+
+ it('should render properly', () => {
+ const { mockDiscoverLocator, mockLogExplorerLocator } = renderTabs();
+ expect(getDiscoverTab()).toBeInTheDocument();
+ expect(mockDiscoverLocator.getRedirectUrl).toHaveBeenCalledWith({});
+ expect(getDiscoverTab()).toHaveAttribute('href', DISCOVER_APP_LOCATOR);
+ expect(getLogExplorerTab()).toBeInTheDocument();
+ expect(mockLogExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({});
+ expect(getLogExplorerTab()).toHaveAttribute('href', ALL_DATASETS_LOCATOR_ID);
+ });
+
+ it('should render Discover as the selected tab', () => {
+ const { mockDiscoverLocator, mockLogExplorerLocator } = renderTabs();
+ expect(getDiscoverTab()).toHaveAttribute('aria-selected', 'true');
+ userEvent.click(getDiscoverTab());
+ expect(mockDiscoverLocator.navigate).not.toHaveBeenCalled();
+ expect(getLogExplorerTab()).toHaveAttribute('aria-selected', 'false');
+ userEvent.click(getLogExplorerTab());
+ expect(mockLogExplorerLocator.navigate).toHaveBeenCalledWith({});
+ });
+
+ it('should render Log Explorer as the selected tab', () => {
+ const { mockDiscoverLocator, mockLogExplorerLocator } = renderTabs('log-explorer');
+ expect(getLogExplorerTab()).toHaveAttribute('aria-selected', 'true');
+ userEvent.click(getLogExplorerTab());
+ expect(mockLogExplorerLocator.navigate).not.toHaveBeenCalled();
+ expect(getDiscoverTab()).toHaveAttribute('aria-selected', 'false');
+ userEvent.click(getDiscoverTab());
+ expect(mockDiscoverLocator.navigate).toHaveBeenCalledWith({});
+ });
+});
diff --git a/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.tsx b/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.tsx
new file mode 100644
index 00000000000000..ea9504d6b7ba1e
--- /dev/null
+++ b/src/plugins/discover/public/components/log_explorer_tabs/log_explorer_tabs.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui';
+import { AllDatasetsLocatorParams, ALL_DATASETS_LOCATOR_ID } from '@kbn/deeplinks-observability';
+import { i18n } from '@kbn/i18n';
+import React, { MouseEvent } from 'react';
+import { DiscoverAppLocatorParams, DISCOVER_APP_LOCATOR } from '../../../common';
+import type { DiscoverServices } from '../../build_services';
+
+export interface LogExplorerTabsProps {
+ services: Pick;
+ selectedTab: 'discover' | 'log-explorer';
+}
+
+const emptyParams = {};
+
+export const LogExplorerTabs = ({ services, selectedTab }: LogExplorerTabsProps) => {
+ const { euiTheme } = useEuiTheme();
+ const locators = services.share?.url.locators;
+ const discoverLocator = locators?.get(DISCOVER_APP_LOCATOR);
+ const logExplorerLocator = locators?.get(ALL_DATASETS_LOCATOR_ID);
+ const discoverUrl = discoverLocator?.getRedirectUrl(emptyParams);
+ const logExplorerUrl = logExplorerLocator?.getRedirectUrl(emptyParams);
+
+ const navigateToDiscover = createNavigateHandler(() => {
+ if (selectedTab !== 'discover') {
+ discoverLocator?.navigate(emptyParams);
+ }
+ });
+
+ const navigateToLogExplorer = createNavigateHandler(() => {
+ if (selectedTab !== 'log-explorer') {
+ logExplorerLocator?.navigate(emptyParams);
+ }
+ });
+
+ return (
+
+
+ {i18n.translate('discover.logExplorerTabs.discover', {
+ defaultMessage: 'Discover',
+ })}
+
+
+ {i18n.translate('discover.logExplorerTabs.logExplorer', {
+ defaultMessage: 'Logs Explorer',
+ })}
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default LogExplorerTabs;
+
+const isModifiedEvent = (event: MouseEvent) =>
+ event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
+
+const isLeftClickEvent = (event: MouseEvent) => event.button === 0;
+
+const createNavigateHandler = (onClick: () => void) => (e: MouseEvent) => {
+ if (isModifiedEvent(e) || !isLeftClickEvent(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ onClick();
+};
diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts
index 37a46f30c4ccfb..e454798225d2f5 100644
--- a/src/plugins/discover/public/index.ts
+++ b/src/plugins/discover/public/index.ts
@@ -34,3 +34,4 @@ export type {
} from './customizations';
export { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './embeddable';
export { loadSharingDataHelpers } from './utils';
+export { LogExplorerTabs, type LogExplorerTabsProps } from './components/log_explorer_tabs';
diff --git a/src/plugins/discover/public/mocks.tsx b/src/plugins/discover/public/mocks.tsx
index e97c8f5a841a24..bef23a464a793c 100644
--- a/src/plugins/discover/public/mocks.tsx
+++ b/src/plugins/discover/public/mocks.tsx
@@ -17,6 +17,7 @@ export type Start = jest.Mocked;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
locator: sharePluginMock.createLocator(),
+ showLogExplorerTabs: jest.fn(),
};
return setupContract;
};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 88603a1a755d40..dc4e92e45d0bf4 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -46,6 +46,7 @@ import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import { TRUNCATE_MAX_HEIGHT, ENABLE_ESQL } from '@kbn/discover-utils';
import { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
+import type { ServerlessPluginStart } from '@kbn/serverless/public';
import { PLUGIN_ID } from '../common';
import {
setHeaderActionMenuMounter,
@@ -116,6 +117,7 @@ export interface DiscoverSetup {
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
+ readonly showLogExplorerTabs: () => void;
}
export interface DiscoverStart {
@@ -197,6 +199,7 @@ export interface DiscoverStartPlugins {
lens: LensPublicStart;
contentManagement: ContentManagementPublicStart;
noDataPage?: NoDataPagePluginStart;
+ serverless?: ServerlessPluginStart;
}
/**
@@ -214,6 +217,7 @@ export class DiscoverPlugin
private locator?: DiscoverAppLocator;
private contextLocator?: DiscoverContextAppLocator;
private singleDocLocator?: DiscoverSingleDocLocator;
+ private showLogExplorerTabs = false;
setup(core: CoreSetup, plugins: DiscoverSetupPlugins) {
const baseUrl = core.http.basePath.prepend('/app/discover');
@@ -318,18 +322,24 @@ export class DiscoverPlugin
);
// make sure the data view list is up to date
- await discoverStartPlugins.dataViews.clearCache();
+ discoverStartPlugins.dataViews.clearCache();
- const { renderApp } = await import('./application');
// FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown
// due to EUI bug https://github.com/elastic/eui/pull/5152
params.element.classList.add('dscAppWrapper');
+
+ const { renderApp } = await import('./application');
const unmount = renderApp({
element: params.element,
services,
profileRegistry: this.profileRegistry,
+ customizationContext: {
+ displayMode: 'standalone',
+ showLogExplorerTabs: this.showLogExplorerTabs,
+ },
isDev,
});
+
return () => {
unlistenParentHistory();
unmount();
@@ -368,6 +378,9 @@ export class DiscoverPlugin
return {
locator: this.locator,
+ showLogExplorerTabs: () => {
+ this.showLogExplorerTabs = true;
+ },
};
}
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index d4b09ed3938e3f..fe06c932324602 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -77,7 +77,9 @@
"@kbn/unsaved-changes-badge",
"@kbn/rule-data-utils",
"@kbn/core-chrome-browser",
- "@kbn/core-plugins-server"
+ "@kbn/core-plugins-server",
+ "@kbn/serverless",
+ "@kbn/deeplinks-observability"
],
"exclude": ["target/**/*"]
}
diff --git a/src/plugins/navigation/public/index.ts b/src/plugins/navigation/public/index.ts
index 7db4526995c04c..65b8851a39ac20 100644
--- a/src/plugins/navigation/public/index.ts
+++ b/src/plugins/navigation/public/index.ts
@@ -14,7 +14,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export type { TopNavMenuData, TopNavMenuProps, TopNavMenuBadgeProps } from './top_nav_menu';
-export { TopNavMenu } from './top_nav_menu';
+export { TopNavMenu, TopNavMenuItems, TopNavMenuBadges } from './top_nav_menu';
export type { NavigationPublicPluginSetup, NavigationPublicPluginStart } from './types';
diff --git a/src/plugins/navigation/public/top_nav_menu/index.ts b/src/plugins/navigation/public/top_nav_menu/index.ts
index e6705273c2a1b6..24986f3acb3baa 100644
--- a/src/plugins/navigation/public/top_nav_menu/index.ts
+++ b/src/plugins/navigation/public/top_nav_menu/index.ts
@@ -7,8 +7,10 @@
*/
export { createTopNav } from './create_top_nav_menu';
-export type { TopNavMenuProps, TopNavMenuBadgeProps } from './top_nav_menu';
+export type { TopNavMenuProps } from './top_nav_menu';
export { TopNavMenu } from './top_nav_menu';
export type { TopNavMenuData } from './top_nav_menu_data';
export type { TopNavMenuExtensionsRegistrySetup } from './top_nav_menu_extensions_registry';
export { TopNavMenuExtensionsRegistry } from './top_nav_menu_extensions_registry';
+export { TopNavMenuItems } from './top_nav_menu_items';
+export { TopNavMenuBadges, type TopNavMenuBadgeProps } from './top_nav_menu_badges';
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
index 218c620519cf2c..376d0c4a7b442d 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
@@ -10,11 +10,12 @@ import React from 'react';
import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { MountPoint } from '@kbn/core/public';
-import { TopNavMenu, TopNavMenuBadgeProps } from './top_nav_menu';
+import { TopNavMenu } from './top_nav_menu';
import { TopNavMenuData } from './top_nav_menu_data';
-import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers';
+import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { EuiToolTipProps } from '@elastic/eui';
+import type { TopNavMenuBadgeProps } from './top_nav_menu_badges';
const unifiedSearch = {
ui: {
@@ -66,35 +67,35 @@ describe('TopNavMenu', () => {
];
it('Should render nothing when no config is provided', () => {
- const component = shallowWithIntl();
+ const component = mountWithIntl();
expect(component.find(WRAPPER_SELECTOR).length).toBe(0);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should not render menu items when config is empty', () => {
- const component = shallowWithIntl();
+ const component = mountWithIntl();
expect(component.find(WRAPPER_SELECTOR).length).toBe(0);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should render 1 menu item', () => {
- const component = shallowWithIntl();
+ const component = mountWithIntl();
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(1);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should render multiple menu items', () => {
- const component = shallowWithIntl();
+ const component = mountWithIntl();
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
});
it('Should render search bar', () => {
- const component = shallowWithIntl(
+ const component = mountWithIntl(
);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
@@ -103,7 +104,7 @@ describe('TopNavMenu', () => {
});
it('Should render menu items and search bar', () => {
- const component = shallowWithIntl(
+ const component = mountWithIntl(
{
});
it('Should render with a class name', () => {
- const component = shallowWithIntl(
+ const component = mountWithIntl(
{
className={'myCoolClass'}
/>
);
- expect(component.find('.kbnTopNavMenu').length).toBe(1);
+ expect(findTestSubject(component, 'top-nav').hasClass('kbnTopNavMenu')).toBe(true);
expect(component.find('.myCoolClass').length).toBeTruthy();
});
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
index 9c899cd9d7207f..56daf31fb0703a 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
@@ -6,15 +6,7 @@
* Side Public License, v 1.
*/
-import React, { ReactElement, Fragment } from 'react';
-import {
- EuiBadge,
- EuiBadgeGroup,
- EuiBadgeProps,
- EuiHeaderLinks,
- EuiToolTip,
- EuiToolTipProps,
-} from '@elastic/eui';
+import React, { ReactElement } from 'react';
import classNames from 'classnames';
import { MountPoint } from '@kbn/core/public';
@@ -23,13 +15,8 @@ import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/publi
import { StatefulSearchBarProps } from '@kbn/unified-search-plugin/public';
import { AggregateQuery, Query } from '@kbn/es-query';
import { TopNavMenuData } from './top_nav_menu_data';
-import { TopNavMenuItem } from './top_nav_menu_item';
-
-export type TopNavMenuBadgeProps = EuiBadgeProps & {
- badgeText: string;
- toolTipProps?: Partial;
- renderCustomBadge?: (props: { badgeText: string }) => ReactElement;
-};
+import { TopNavMenuItems } from './top_nav_menu_items';
+import { TopNavMenuBadgeProps, TopNavMenuBadges } from './top_nav_menu_badges';
export type TopNavMenuProps = Omit<
StatefulSearchBarProps,
@@ -83,54 +70,12 @@ export function TopNavMenu(
return null;
}
- function createBadge(
- { badgeText, toolTipProps, renderCustomBadge, ...badgeProps }: TopNavMenuBadgeProps,
- i: number
- ): ReactElement {
- const key = `nav-menu-badge-${i}`;
-
- const Badge = () => (
-
- {badgeText}
-
- );
-
- if (renderCustomBadge) {
- return {renderCustomBadge({ badgeText })};
- }
-
- return toolTipProps ? (
-
-
-
- ) : (
-
- );
- }
-
function renderBadges(): ReactElement | null {
- if (!badges || badges.length === 0) return null;
- return (
-
- {badges.map(createBadge)}
-
- );
- }
-
- function renderItems(): ReactElement[] | null {
- if (!config || config.length === 0) return null;
- return config.map((menuItem: TopNavMenuData, i: number) => {
- return ;
- });
+ return ;
}
function renderMenu(className: string): ReactElement | null {
- if (!config || config.length === 0) return null;
- return (
-
- {renderItems()}
-
- );
+ return ;
}
function renderSearchBar(): ReactElement | null {
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_badges.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_badges.tsx
new file mode 100644
index 00000000000000..f1c3a08e584180
--- /dev/null
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_badges.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiBadge, EuiBadgeGroup, EuiToolTip, EuiBadgeProps, EuiToolTipProps } from '@elastic/eui';
+import React, { Fragment, ReactElement } from 'react';
+
+export type TopNavMenuBadgeProps = EuiBadgeProps & {
+ badgeText: string;
+ toolTipProps?: Partial;
+ renderCustomBadge?: (props: { badgeText: string }) => ReactElement;
+};
+
+export const TopNavMenuBadges = ({ badges }: { badges: TopNavMenuBadgeProps[] | undefined }) => {
+ if (!badges || badges.length === 0) return null;
+ return (
+ {badges.map(createBadge)}
+ );
+};
+
+function createBadge(
+ { badgeText, toolTipProps, renderCustomBadge, ...badgeProps }: TopNavMenuBadgeProps,
+ i: number
+): ReactElement {
+ const key = `nav-menu-badge-${i}`;
+
+ const Badge = () => (
+
+ {badgeText}
+
+ );
+
+ if (renderCustomBadge) {
+ return {renderCustomBadge({ badgeText })};
+ }
+
+ return toolTipProps ? (
+
+
+
+ ) : (
+
+ );
+}
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx
new file mode 100644
index 00000000000000..3113bdb4c1ce7b
--- /dev/null
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiHeaderLinks } from '@elastic/eui';
+import React from 'react';
+import type { TopNavMenuData } from './top_nav_menu_data';
+import { TopNavMenuItem } from './top_nav_menu_item';
+
+export const TopNavMenuItems = ({
+ config,
+ className,
+}: {
+ config: TopNavMenuData[] | undefined;
+ className?: string;
+}) => {
+ if (!config || config.length === 0) return null;
+ return (
+
+ {config.map((menuItem: TopNavMenuData, i: number) => {
+ return ;
+ })}
+
+ );
+};
diff --git a/x-pack/plugins/observability_log_explorer/common/translations.ts b/x-pack/plugins/observability_log_explorer/common/translations.ts
index d04c9942c9cbcb..fb810311d134d0 100644
--- a/x-pack/plugins/observability_log_explorer/common/translations.ts
+++ b/x-pack/plugins/observability_log_explorer/common/translations.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
export const logExplorerAppTitle = i18n.translate('xpack.observabilityLogExplorer.appTitle', {
- defaultMessage: 'Log Explorer',
+ defaultMessage: 'Logs Explorer',
});
export const logsAppTitle = i18n.translate('xpack.observabilityLogExplorer.logsAppTitle', {
diff --git a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx
index 205fc824e409f1..c627f0760bb2d6 100644
--- a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx
+++ b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx
@@ -30,6 +30,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { css } from '@emotion/react';
import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common';
import { euiThemeVars } from '@kbn/ui-theme';
+import { LogExplorerTabs } from '@kbn/discover-plugin/public';
import { PluginKibanaContextValue } from '../utils/use_kibana';
import {
betaBadgeDescription,
@@ -73,12 +74,10 @@ const ServerlessTopNav = ({
state$,
}: Pick) => {
return (
-
+
-
-
-
+
-
+
+
+
+
@@ -177,31 +179,7 @@ const StatefulTopNav = ({
const DiscoverLink = React.memo(
({ services, state$ }: Pick) => {
- const { appState, logExplorerState } = useObservable(
- state$.pipe(
- distinctUntilChanged((prev, curr) => {
- if (!prev.appState || !curr.appState) return false;
- return deepEqual(
- [
- prev.appState.columns,
- prev.appState.filters,
- prev.appState.index,
- prev.appState.query,
- ],
- [curr.appState.columns, curr.appState.filters, curr.appState.index, curr.appState.query]
- );
- })
- ),
- { appState: {}, logExplorerState: {} }
- );
-
- const discoverLinkParams = {
- columns: appState?.columns,
- filters: appState?.filters,
- query: appState?.query,
- dataViewSpec: logExplorerState?.datasetSelection?.selection.dataset.toDataviewSpec(),
- };
-
+ const discoverLinkParams = useDiscoverLinkParams(state$);
const discoverUrl = services.discover.locator?.getRedirectUrl(discoverLinkParams);
const navigateToDiscover = () => {
@@ -217,7 +195,6 @@ const DiscoverLink = React.memo(
{discoverLinkTitle}
@@ -275,3 +252,38 @@ const VerticalRule = styled.span`
height: 20px;
background-color: ${euiThemeVars.euiColorLightShade};
`;
+
+const useDiscoverLinkParams = (state$: BehaviorSubject) => {
+ const { appState, logExplorerState } = useObservable(
+ state$.pipe(
+ distinctUntilChanged((prev, curr) => {
+ if (!prev.appState || !curr.appState) return false;
+ return deepEqual(
+ [
+ prev.appState.columns,
+ prev.appState.sort,
+ prev.appState.filters,
+ prev.appState.index,
+ prev.appState.query,
+ ],
+ [
+ curr.appState.columns,
+ curr.appState.sort,
+ curr.appState.filters,
+ curr.appState.index,
+ curr.appState.query,
+ ]
+ );
+ })
+ ),
+ { appState: {}, logExplorerState: {} }
+ );
+
+ return {
+ columns: appState?.columns,
+ sort: appState?.sort,
+ filters: appState?.filters,
+ query: appState?.query,
+ dataViewSpec: logExplorerState?.datasetSelection?.selection.dataset.toDataviewSpec(),
+ };
+};
diff --git a/x-pack/plugins/observability_log_explorer/public/plugin.ts b/x-pack/plugins/observability_log_explorer/public/plugin.ts
index 755072ad9786d9..5b5a640e2a6aee 100644
--- a/x-pack/plugins/observability_log_explorer/public/plugin.ts
+++ b/x-pack/plugins/observability_log_explorer/public/plugin.ts
@@ -44,7 +44,7 @@ export class ObservabilityLogExplorerPlugin
core: CoreSetup,
_pluginsSetup: ObservabilityLogExplorerSetupDeps
) {
- const { share } = _pluginsSetup;
+ const { share, serverless, discover } = _pluginsSetup;
const useHash = core.uiSettings.get('state:storeInSessionStorage');
core.application.register({
@@ -69,6 +69,10 @@ export class ObservabilityLogExplorerPlugin
},
});
+ if (serverless) {
+ discover.showLogExplorerTabs();
+ }
+
// Register Locators
const singleDatasetLocator = share.url.locators.create(
new SingleDatasetLocatorDefinition({
diff --git a/x-pack/plugins/observability_log_explorer/public/types.ts b/x-pack/plugins/observability_log_explorer/public/types.ts
index 5f455088b7442c..245e8227c72b06 100644
--- a/x-pack/plugins/observability_log_explorer/public/types.ts
+++ b/x-pack/plugins/observability_log_explorer/public/types.ts
@@ -7,7 +7,7 @@
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { LogExplorerPluginStart } from '@kbn/log-explorer-plugin/public';
-import { DiscoverStart } from '@kbn/discover-plugin/public';
+import { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import { ServerlessPluginStart } from '@kbn/serverless/public';
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
@@ -27,6 +27,7 @@ export interface ObservabilityLogExplorerPluginSetup {
export interface ObservabilityLogExplorerPluginStart {}
export interface ObservabilityLogExplorerSetupDeps {
+ discover: DiscoverSetup;
serverless?: ServerlessPluginStart;
share: SharePluginSetup;
}
diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx
index d53281dbf6de13..2cca4b3ea78928 100644
--- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx
+++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx
@@ -29,15 +29,15 @@ const navigationTree: NavigationTreeDefinition = {
breadcrumbStatus: 'hidden',
children: [
{
- title: i18n.translate('xpack.serverlessObservability.nav.logExplorer', {
- defaultMessage: 'Log Explorer',
+ title: i18n.translate('xpack.serverlessObservability.nav.discover', {
+ defaultMessage: 'Discover',
}),
- link: 'observability-log-explorer',
+ link: 'discover',
renderAs: 'item',
children: [
{
- // This is to show "discover" breadcrumbs when navigating from "log explorer" to "discover"
- link: 'discover',
+ // This is to show "observability-log-explorer" breadcrumbs when navigating from "discover" to "log explorer"
+ link: 'observability-log-explorer',
sideNavStatus: 'hidden',
},
],
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 00aa8b30278db0..488aa279b4c054 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -43365,7 +43365,6 @@
"xpack.serverlessObservability.nav.devTools": "Outils de développeur",
"xpack.serverlessObservability.nav.getStarted": "Ajouter des données",
"xpack.serverlessObservability.nav.infrastructure": "Infrastructure",
- "xpack.serverlessObservability.nav.logExplorer": "Explorateur de log",
"xpack.serverlessObservability.nav.ml.job.notifications": "Notifications de tâches",
"xpack.serverlessObservability.nav.ml.jobs": "Détection des anomalies",
"xpack.serverlessObservability.nav.mngt": "Gestion",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 36660d0392e6b2..c09f29875cb579 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -43356,7 +43356,6 @@
"xpack.serverlessObservability.nav.devTools": "開発者ツール",
"xpack.serverlessObservability.nav.getStarted": "データの追加",
"xpack.serverlessObservability.nav.infrastructure": "インフラストラクチャー",
- "xpack.serverlessObservability.nav.logExplorer": "ログエクスプローラー",
"xpack.serverlessObservability.nav.ml.job.notifications": "ジョブ通知",
"xpack.serverlessObservability.nav.ml.jobs": "異常検知",
"xpack.serverlessObservability.nav.mngt": "管理",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 84bf8c04333b53..6b809caf229bda 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -43349,7 +43349,6 @@
"xpack.serverlessObservability.nav.devTools": "开发者工具",
"xpack.serverlessObservability.nav.getStarted": "添加数据",
"xpack.serverlessObservability.nav.infrastructure": "基础设施",
- "xpack.serverlessObservability.nav.logExplorer": "日志浏览器",
"xpack.serverlessObservability.nav.ml.job.notifications": "作业通知",
"xpack.serverlessObservability.nav.ml.jobs": "异常检测",
"xpack.serverlessObservability.nav.mngt": "管理",
diff --git a/x-pack/test/functional/apps/observability_log_explorer/app.ts b/x-pack/test/functional/apps/observability_log_explorer/app.ts
index c60bf38205e3a7..5d6c85e3c75189 100644
--- a/x-pack/test/functional/apps/observability_log_explorer/app.ts
+++ b/x-pack/test/functional/apps/observability_log_explorer/app.ts
@@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.navigationalSearch.searchFor('log explorer');
const results = await PageObjects.navigationalSearch.getDisplayedResults();
- expect(results[0].label).to.eql('Log Explorer');
+ expect(results[0].label).to.eql('Logs Explorer');
});
it('is shown in the observability side navigation', async () => {
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts
index 13629a6007937f..c0dc9c0dd89f1e 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts
@@ -77,9 +77,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('Side nav', function () {
- // Discover does not exist in Serverless O11y side nav (Log Explorer instead)
- this.tags('skipSvlOblt');
-
it('should sync Lens global state to Discover sidebar link and carry over the state when navigating to Discover', async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.common.navigateToApp('lens');
diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts
index b3d6c225f50c4e..8453042cc71617 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts
@@ -13,7 +13,7 @@ describe.skip('Serverless', () => {
it('contains the side navigation for observabilitity serverless', () => {
cy.loginAsElasticUser();
- cy.contains('Log Explorer');
+ cy.contains('Logs Explorer');
cy.contains('Dashboards');
cy.contains('Alerts');
cy.contains('AIOps');
@@ -26,7 +26,7 @@ describe.skip('Serverless', () => {
it('navigates to discover-dashboard-viz links', () => {
cy.loginAsElasticUser();
- cy.contains('Log Explorer').click();
+ cy.contains('Logs Explorer').click();
cy.url().should('include', '/app/observability-log-explorer');
cy.contains('Dashboards').click();
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 a295d93e009bc0..892359f901ffe5 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts
@@ -41,15 +41,11 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
});
await svlCommonNavigation.sidenav.expectSectionClosed('project_settings_project_nav');
- // navigate to log explorer
- await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-log-explorer' });
- await svlCommonNavigation.sidenav.expectLinkActive({
- deepLinkId: 'observability-log-explorer',
- });
- await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
- deepLinkId: 'observability-log-explorer',
- });
- await expect(await browser.getCurrentUrl()).contain('/app/observability-log-explorer');
+ // navigate to discover
+ await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });
+ await svlCommonNavigation.sidenav.expectLinkActive({ deepLinkId: 'discover' });
+ await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'discover' });
+ expect(await browser.getCurrentUrl()).contain('/app/discover');
// check the aiops subsection
await svlCommonNavigation.sidenav.openSection('observability_project_nav.aiops'); // open ai ops subsection
diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts
index 271bba6b45035f..4a1cf5024ff4d4 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/app.ts
@@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.svlCommonNavigation.search.searchFor('log explorer');
const results = await PageObjects.svlCommonNavigation.search.getDisplayedResults();
- expect(results[0].label).to.eql('Log Explorer');
+ expect(results[0].label).to.eql('Logs Explorer');
await PageObjects.svlCommonNavigation.search.hideSearch();
});
diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts
index c9f8e598ff2a89..0e7527bc76887c 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts
@@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'svlCommonPage',
'timePicker',
'header',
+ 'svlCommonNavigation',
]);
describe('Header menu', () => {
@@ -27,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.load(
'x-pack/test/functional/es_archives/observability_log_explorer/data_streams'
);
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await PageObjects.svlCommonPage.login();
await PageObjects.observabilityLogExplorer.navigateTo();
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -38,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.unload(
'x-pack/test/functional/es_archives/observability_log_explorer/data_streams'
);
+ await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
it('should inject the app header menu on the top navbar', async () => {
@@ -61,9 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.existOrFail('superDatePickerstartDatePopoverButton');
await testSubjects.existOrFail('superDatePickerendDatePopoverButton');
});
-
const timeConfig = await PageObjects.timePicker.getTimeConfig();
-
// Set query bar value
await PageObjects.observabilityLogExplorer.submitQuery('*favicon*');
@@ -75,7 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql('All logs');
});
-
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
@@ -84,17 +84,80 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'message',
]);
});
-
await retry.try(async () => {
expect(await PageObjects.timePicker.getTimeConfig()).to.eql(timeConfig);
});
-
await retry.try(async () => {
expect(await PageObjects.observabilityLogExplorer.getQueryBarValue()).to.eql('*favicon*');
});
});
});
+ describe('Discover tabs', () => {
+ before(async () => {
+ await PageObjects.observabilityLogExplorer.navigateTo();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ it('should navigate between discover tabs without keeping the current columns/filters/query/time/data view', async () => {
+ await retry.try(async () => {
+ await testSubjects.existOrFail('superDatePickerstartDatePopoverButton');
+ await testSubjects.existOrFail('superDatePickerendDatePopoverButton');
+ });
+
+ const timeConfig = await PageObjects.timePicker.getTimeConfig();
+
+ // Set query bar value
+ await PageObjects.observabilityLogExplorer.submitQuery('*favicon*');
+
+ // go to discover tab
+ await testSubjects.click('discoverTab');
+ await PageObjects.svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
+ deepLinkId: 'discover',
+ });
+ await PageObjects.svlCommonNavigation.breadcrumbs.expectBreadcrumbMissing({
+ deepLinkId: 'observability-log-explorer',
+ });
+ expect(await browser.getCurrentUrl()).contain('/app/discover');
+
+ await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
+ await PageObjects.discover.waitForDocTableLoadingComplete();
+
+ await retry.try(async () => {
+ expect(await PageObjects.discover.getCurrentlySelectedDataView()).not.to.eql('All logs');
+ });
+
+ await retry.try(async () => {
+ expect(await PageObjects.discover.getColumnHeaders()).not.to.eql([
+ '@timestamp',
+ 'service.name',
+ 'host.name',
+ 'message',
+ ]);
+ });
+
+ await retry.try(async () => {
+ expect(await PageObjects.timePicker.getTimeConfig()).not.to.eql(timeConfig);
+ });
+
+ await retry.try(async () => {
+ expect(await PageObjects.observabilityLogExplorer.getQueryBarValue()).not.to.eql(
+ '*favicon*'
+ );
+ });
+
+ // go to log explorer tab
+ await testSubjects.click('logExplorerTab');
+ await PageObjects.svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
+ deepLinkId: 'discover',
+ });
+ await PageObjects.svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({
+ deepLinkId: 'observability-log-explorer',
+ });
+ expect(await browser.getCurrentUrl()).contain('/app/observability-log-explorer');
+ });
+ });
+
describe('Add data link', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();