diff --git a/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx b/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx index ece9ebad4b423..21fd8fa95691c 100644 --- a/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx @@ -33,7 +33,7 @@ export const DiscoverGrid: React.FC = ({ rowAdditionalLeadingControls: customRowAdditionalLeadingControls, ...props }) => { - const { dataView, setExpandedDoc } = props; + const { dataView, setExpandedDoc, renderDocumentView } = props; const getRowIndicatorProvider = useProfileAccessor('getRowIndicatorProvider'); const getRowIndicator = useMemo(() => { return getRowIndicatorProvider(() => undefined)({ dataView: props.dataView }); @@ -48,6 +48,7 @@ export const DiscoverGrid: React.FC = ({ query, updateESQLQuery: onUpdateESQLQuery, setExpandedDoc, + isDocViewerEnabled: !!renderDocumentView, }); }, [ customRowAdditionalLeadingControls, @@ -56,6 +57,7 @@ export const DiscoverGrid: React.FC = ({ onUpdateESQLQuery, query, setExpandedDoc, + renderDocumentView, ]); const getPaginationConfigAccessor = useProfileAccessor('getPaginationConfig'); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts index fdc398ae1fa4d..8a69ba8004330 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts @@ -21,7 +21,7 @@ export const getRowAdditionalLeadingControls: LogsDataSourceProfileProvider['pro (prev, { context }) => (params) => { const additionalControls = prev(params) || []; - const { updateESQLQuery, query, setExpandedDoc } = params; + const { updateESQLQuery, query, setExpandedDoc, isDocViewerEnabled } = params; const isDegradedDocsControlEnabled = isOfAggregateQueryType(query) ? queryContainsMetadataIgnored(query) @@ -52,15 +52,17 @@ export const getRowAdditionalLeadingControls: LogsDataSourceProfileProvider['pro setExpandedDoc(props.record, { initialTabId: 'doc_view_logs_overview' }); }; - return [ - ...additionalControls, - createDegradedDocsControl({ - enabled: isDegradedDocsControlEnabled, - addIgnoredMetadataToQuery, - onClick: leadingControlClick('quality_issues'), - }), - createStacktraceControl({ onClick: leadingControlClick('stacktrace') }), - ]; + return isDocViewerEnabled + ? [ + ...additionalControls, + createDegradedDocsControl({ + enabled: isDegradedDocsControlEnabled, + addIgnoredMetadataToQuery, + onClick: leadingControlClick('quality_issues'), + }), + createStacktraceControl({ onClick: leadingControlClick('stacktrace') }), + ] + : additionalControls; }; const queryContainsMetadataIgnored = (query: AggregateQuery) => diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts index 20bad472c32a4..2d2be1561d3ad 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts @@ -254,12 +254,29 @@ describe('logsDataSourceProfileProvider', () => { }); const rowAdditionalLeadingControls = getRowAdditionalLeadingControls?.({ dataView: dataViewWithLogLevel, + isDocViewerEnabled: true, }); expect(rowAdditionalLeadingControls).toHaveLength(2); expect(rowAdditionalLeadingControls?.[0].id).toBe('connectedDegradedDocs'); expect(rowAdditionalLeadingControls?.[1].id).toBe('connectedStacktraceDocs'); }); + + it('should not return the passed additional controls if the flag is turned off', () => { + const getRowAdditionalLeadingControls = + logsDataSourceProfileProvider.profile.getRowAdditionalLeadingControls?.(() => undefined, { + context: { + category: DataSourceCategory.Logs, + logOverviewContext$: new BehaviorSubject(undefined), + }, + }); + const rowAdditionalLeadingControls = getRowAdditionalLeadingControls?.({ + dataView: dataViewWithLogLevel, + isDocViewerEnabled: false, + }); + + expect(rowAdditionalLeadingControls).toHaveLength(0); + }); }); }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/types.ts b/src/platform/plugins/shared/discover/public/context_awareness/types.ts index 2ba18bbc3f138..885e5ce5334b8 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/types.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/types.ts @@ -188,6 +188,10 @@ export interface RowControlsExtensionParams { * @param options.initialTabId - The tabId to display in the flyout */ setExpandedDoc?: (record?: DataTableRecord, options?: { initialTabId?: string }) => void; + /** + * Flag to indicate if Flyout opening controls must be rendered or not + */ + isDocViewerEnabled: boolean; } /** diff --git a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx index 49efc27cd011c..18779d467f31e 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx @@ -91,7 +91,6 @@ export function SearchEmbeddableGridComponent({ // `api.query$` and `api.filters$` are the initial values from the saved search SO (as of now) // `fetchContext.query` and `fetchContext.filters` are Dashboard's query and filters - const savedSearchQuery = apiQuery; const savedSearchFilters = apiFilters; diff --git a/src/platform/plugins/shared/discover/public/embeddable/constants.ts b/src/platform/plugins/shared/discover/public/embeddable/constants.ts index 64028b9cf1b28..f32fd32b3f51a 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/constants.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/constants.ts @@ -23,6 +23,8 @@ export const SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER: Trigger = { 'This trigger is used to replace the cell actions for Discover session embeddable grid.', } as const; +export const LEGACY_LOG_STREAM_EMBEDDABLE = 'LOG_STREAM_EMBEDDABLE'; + export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH'; export const DEFAULT_HEADER_ROW_HEIGHT_LINES = 3; diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts b/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts new file mode 100644 index 0000000000000..5d87e09d8e155 --- /dev/null +++ b/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts @@ -0,0 +1,65 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { getAllLogsDataViewSpec } from '@kbn/discover-utils/src'; +import { toSavedSearchAttributes } from '@kbn/saved-search-plugin/common'; +import { getSearchEmbeddableFactory } from './get_search_embeddable_factory'; +import { LEGACY_LOG_STREAM_EMBEDDABLE } from './constants'; + +export const getLegacyLogStreamEmbeddableFactory = ( + ...[{ startServices, discoverServices }]: Parameters +) => { + const searchEmbeddableFactory = getSearchEmbeddableFactory({ startServices, discoverServices }); + const logStreamEmbeddableFactory: ReturnType = { + type: LEGACY_LOG_STREAM_EMBEDDABLE, + buildEmbeddable: async ({ initialState, ...restParams }) => { + const searchSource = await discoverServices.data.search.searchSource.create(); + let fallbackPattern = 'logs-*-*'; + // Given that the logDataAccess service is an optional dependency with discover, we need to check if it exists + if (discoverServices.logsDataAccess) { + fallbackPattern = + await discoverServices.logsDataAccess.services.logSourcesService.getFlattenedLogSources(); + } + + const spec = getAllLogsDataViewSpec({ allLogsIndexPattern: fallbackPattern }); + const dataView: DataView = await discoverServices.data.dataViews.create(spec); + + // Finally assign the data view to the search source + searchSource.setField('index', dataView); + + const savedSearch: SavedSearch = { + title: initialState.rawState.title, + description: initialState.rawState.description, + timeRange: initialState.rawState.timeRange, + sort: initialState.rawState.sort ?? [], + columns: initialState.rawState.columns ?? [], + searchSource, + managed: false, + }; + const { searchSourceJSON, references } = searchSource.serialize(); + + initialState = { + ...initialState, + rawState: { + ...initialState.rawState, + attributes: { + ...toSavedSearchAttributes(savedSearch, searchSourceJSON), + references, + }, + }, + }; + + return searchEmbeddableFactory.buildEmbeddable({ initialState, ...restParams }); + }, + }; + + return logStreamEmbeddableFactory; +}; diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts index 148a37977776c..1b49db30b7f45 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts @@ -34,85 +34,112 @@ describe('initialize edit api', () => { const waitOneTick = () => new Promise((resolve) => setTimeout(resolve, 0)); describe('get app target', () => { - const runEditLinkTest = async (dataView?: DataView, byValue?: boolean) => { - jest - .spyOn(discoverServiceMock.locator, 'getUrl') - .mockClear() - .mockResolvedValueOnce('/base/mock-url'); - jest - .spyOn(discoverServiceMock.core.http.basePath, 'remove') - .mockClear() - .mockReturnValueOnce('/mock-url'); - - if (dataView) { - mockedApi.dataViews$.next([dataView]); + const runEditLinkTest = async (dataViewInput?: DataView, byValue?: boolean) => { + const currentDataView = dataViewInput || dataViewMock; + // Determine if the current scenario will use a redirect + const currentSavedObjectId = byValue ? undefined : 'test-id'; + const isDataViewPersisted = currentDataView.isPersisted + ? currentDataView.isPersisted() + : true; // Assume persisted if method undefined + const useRedirect = !currentSavedObjectId && !isDataViewPersisted; + + if (useRedirect) { + // This is the "by value with ad hoc data view" (redirect) case. + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/state-url-for-redirect'); // For urlWithoutLocationState + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url'); // For editPath (applied to getRedirectUrl result) } else { - mockedApi.dataViews$.next([dataViewMock]); - } - if (byValue) { - mockedApi.savedObjectId$.next(undefined); - } else { - mockedApi.savedObjectId$.next('test-id'); + // This is a "by reference" or "by value with persisted data view" (non-redirect) case. + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/discover-home') // For getUrl({}) -> urlWithoutLocationState + .mockResolvedValueOnce('/base/mock-url'); // For getUrl(locatorParams) -> raw editUrl + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url'); // For remove('/base/mock-url') -> editPath } + + mockedApi.dataViews$.next([currentDataView]); + mockedApi.savedObjectId$.next(currentSavedObjectId); + await waitOneTick(); const { path: editPath, app: editApp, editUrl, + urlWithoutLocationState, } = await getAppTarget(mockedApi, discoverServiceMock); - return { editPath, editApp, editUrl }; + return { editPath, editApp, editUrl, urlWithoutLocationState }; }; - const testByReference = ({ + const testByReferenceOrNonRedirectValue = ({ editPath, editApp, editUrl, + urlWithoutLocationState, }: { editPath: string; editApp: string; editUrl: string; + urlWithoutLocationState: string; }) => { const locatorParams = getDiscoverLocatorParams(mockedApi); - expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(1); - expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith(locatorParams); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(2); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith({}); // For urlWithoutLocationState + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith(locatorParams); // For raw editUrl + expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledTimes(1); expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledWith('/base/mock-url'); expect(editApp).toBe('discover'); - expect(editPath).toBe('/mock-url'); - expect(editUrl).toBe('/base/mock-url'); + expect(editPath).toBe('/mock-url'); // Result of basePath.remove + expect(editUrl).toBe('/base/mock-url'); // Raw editUrl before basePath.remove + expect(urlWithoutLocationState).toBe('/base/discover-home'); }; it('should correctly output edit link params for by reference saved search', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(); - testByReference({ editPath, editApp, editUrl }); + const result = await runEditLinkTest(dataViewMock, false); + testByReferenceOrNonRedirectValue(result); }); it('should correctly output edit link params for by reference saved search with ad hoc data view', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(dataViewAdHoc); - testByReference({ editPath, editApp, editUrl }); + // Still "by reference" (savedObjectId exists), so no redirect even with ad-hoc data view. + const result = await runEditLinkTest(dataViewAdHoc, false); + testByReferenceOrNonRedirectValue(result); }); - it('should correctly output edit link params for by value saved search', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(undefined, true); - testByReference({ editPath, editApp, editUrl }); + it('should correctly output edit link params for by value saved search (with persisted data view)', async () => { + // "by value" but with a persisted data view (dataViewMock), so no redirect. + const result = await runEditLinkTest(dataViewMock, true); + testByReferenceOrNonRedirectValue(result); }); it('should correctly output edit link params for by value saved search with ad hoc data view', async () => { + // This specific test case mocks getRedirectUrl because it's unique to the redirect flow jest .spyOn(discoverServiceMock.locator, 'getRedirectUrl') .mockClear() - .mockReturnValueOnce('/base/mock-url'); - jest - .spyOn(discoverServiceMock.core.http.basePath, 'remove') - .mockClear() - .mockReturnValueOnce('/mock-url'); + .mockReturnValueOnce('/base/mock-url'); // This will be the raw editUrl - const { editPath, editApp, editUrl } = await runEditLinkTest(dataViewAdHoc, true); + const result = await runEditLinkTest(dataViewAdHoc, true); + const { editPath, editApp, editUrl, urlWithoutLocationState } = result; const locatorParams = getDiscoverLocatorParams(mockedApi); + + // Assertions for urlWithoutLocationState part (getUrl({})) + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(1); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith({}); + + // Assertions for redirect part (getRedirectUrl and basePath.remove) expect(discoverServiceMock.locator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(discoverServiceMock.locator.getRedirectUrl).toHaveBeenCalledWith(locatorParams); expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledTimes(1); @@ -121,6 +148,7 @@ describe('initialize edit api', () => { expect(editApp).toBe('r'); expect(editPath).toBe('/mock-url'); expect(editUrl).toBe('/base/mock-url'); + expect(urlWithoutLocationState).toBe('/base/state-url-for-redirect'); }); }); @@ -130,13 +158,26 @@ describe('initialize edit api', () => { navigateToEditor: mockedNavigate, })); mockedApi.dataViews$.next([dataViewMock]); + mockedApi.savedObjectId$.next('test-id'); // Assuming a by-reference scenario for onEdit await waitOneTick(); + // Mocking for getAppTarget call within onEdit + // Assuming a non-redirect case for simplicity + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/discover-home-for-onedit') // For getUrl({}) + .mockResolvedValueOnce('/base/mock-url-for-onedit'); // For getUrl(locatorParams) + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url-for-onedit'); + const { onEdit } = initializeEditApi({ uuid: 'test', parentApi: { - getAppContext: jest.fn().mockResolvedValue({ - getCurrentPath: jest.fn(), + getAppContext: jest.fn().mockReturnValue({ + getCurrentPath: jest.fn().mockReturnValue('/current-parent-path'), currentAppId: 'dashboard', }), }, @@ -148,8 +189,12 @@ describe('initialize edit api', () => { await onEdit(); expect(mockedNavigate).toBeCalledTimes(1); expect(mockedNavigate).toBeCalledWith('discover', { - path: '/mock-url', - state: expect.any(Object), + path: '/mock-url-for-onedit', + state: expect.objectContaining({ + embeddableId: 'test', + originatingApp: 'dashboard', + originatingPath: '/current-parent-path', + }), }); }); }); diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts index 10059f60c3d32..0f5db07fe6aa2 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts @@ -36,13 +36,17 @@ export async function getAppTarget( // We need to use a redirect URL if this is a by value saved search using // an ad hoc data view to ensure the data view spec gets encoded in the URL const useRedirect = !savedObjectId && !dataViews?.[0]?.isPersisted(); + + const urlWithoutLocationState = await discoverServices.locator.getUrl({}); + const editUrl = useRedirect ? discoverServices.locator.getRedirectUrl(locatorParams) : await discoverServices.locator.getUrl(locatorParams); + const editPath = discoverServices.core.http.basePath.remove(editUrl); const editApp = useRedirect ? 'r' : 'discover'; - return { path: editPath, app: editApp, editUrl }; + return { path: editPath, app: editApp, editUrl, urlWithoutLocationState }; } export function initializeEditApi< diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts index 15681c5074ac0..909bc1315bf12 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts @@ -64,7 +64,7 @@ const getExecutionContext = async ( api: SavedSearchPartialFetchApi, discoverServices: DiscoverServices ) => { - const { editUrl } = await getAppTarget(api, discoverServices); + const { editUrl, urlWithoutLocationState } = await getAppTarget(api, discoverServices); const childContext: KibanaExecutionContext = { type: SEARCH_EMBEDDABLE_TYPE, name: 'discover', @@ -72,14 +72,41 @@ const getExecutionContext = async ( description: api.title$?.getValue() || api.defaultTitle$?.getValue() || '', url: editUrl, }; - const executionContext = - apiHasParentApi(api) && apiHasExecutionContext(api.parentApi) + const generateExecutionContext = createExecutionContext(api); + const executionContext = generateExecutionContext(childContext); + + if (isExecutionContextWithinLimits(executionContext)) { + return executionContext; + } + + const newChildContext: KibanaExecutionContext = { + ...childContext, + url: urlWithoutLocationState, + }; + return generateExecutionContext(newChildContext); +}; + +const createExecutionContext = + (api: SavedSearchPartialFetchApi) => + (childContext: KibanaExecutionContext): KibanaExecutionContext => { + return apiHasParentApi(api) && apiHasExecutionContext(api.parentApi) ? { ...api.parentApi?.executionContext, child: childContext, } : childContext; - return executionContext; + }; + +const isExecutionContextWithinLimits = (executionContext: KibanaExecutionContext) => { + const value = JSON.stringify(executionContext); + const encoded = encodeURIComponent(value); + + // The max value is set to this arbitrary number because of the following reasons: + // 1. Maximum allowed length of the `baggage` header via which the execution context is passed is 4096 / 4 = 1024 characters. + // 2. The Execution Context Service adds labels (name, page and id) to the context additionally, which can increase the length + // Hence as a safe limit, we set the maximum length of the execution context to 900 characters. + const MAX_VALUE_ALLOWED = 900; + return encoded.length < MAX_VALUE_ALLOWED; }; export function initializeFetch({ diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx index be09d6861c6eb..1b906fede033a 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx @@ -46,6 +46,7 @@ const initializeSearchSource = async ( ]); searchSource.setParent(parentSearchSource); const dataView = searchSource.getField('index'); + return { searchSource, dataView }; }; diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts index 9a3d9c2307ff3..01a9b9c83700d 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts @@ -63,13 +63,12 @@ export const deserializeState = async ({ } else { // by value const { byValueToSavedSearch } = discoverServices.savedSearch; - const savedSearch = await byValueToSavedSearch( - inject( - serializedState.rawState as unknown as EmbeddableStateWithType, - serializedState.references ?? [] - ) as SavedSearchUnwrapResult, - true - ); + const savedSearchUnwrappedResult = inject( + serializedState.rawState as unknown as EmbeddableStateWithType, + serializedState.references ?? [] + ) as SavedSearchUnwrapResult; + + const savedSearch = await byValueToSavedSearch(savedSearchUnwrappedResult, true); return { ...savedSearch, ...panelState, diff --git a/src/platform/plugins/shared/discover/public/plugin.tsx b/src/platform/plugins/shared/discover/public/plugin.tsx index 3c580987f39b6..76fe32c0aee4f 100644 --- a/src/platform/plugins/shared/discover/public/plugin.tsx +++ b/src/platform/plugins/shared/discover/public/plugin.tsx @@ -42,6 +42,7 @@ import { defaultCustomizationContext } from './customizations/defaults'; import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER, ACTION_VIEW_SAVED_SEARCH, + LEGACY_LOG_STREAM_EMBEDDABLE, } from './embeddable/constants'; import { DiscoverContainerInternal, @@ -428,6 +429,21 @@ export class DiscoverPlugin discoverServices, }); }); + + // We register a specialized saved search embeddable factory for the log stream embeddable to support old log stream panels. + plugins.embeddable.registerReactEmbeddableFactory(LEGACY_LOG_STREAM_EMBEDDABLE, async () => { + const [startServices, discoverServices, { getLegacyLogStreamEmbeddableFactory }] = + await Promise.all([ + getStartServices(), + getDiscoverServicesForEmbeddable(), + getEmbeddableServices(), + ]); + + return getLegacyLogStreamEmbeddableFactory({ + startServices, + discoverServices, + }); + }); } } diff --git a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts index 7d9cff7eb303c..6379bfd0d17f4 100644 --- a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts +++ b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts @@ -9,3 +9,4 @@ export { ViewSavedSearchAction } from '../embeddable/actions/view_saved_search_action'; export { getSearchEmbeddableFactory } from '../embeddable/get_search_embeddable_factory'; +export { getLegacyLogStreamEmbeddableFactory } from '../embeddable/get_legacy_log_stream_embeddable_factory'; diff --git a/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts b/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts new file mode 100644 index 0000000000000..391584a580266 --- /dev/null +++ b/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts @@ -0,0 +1,75 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import moment from 'moment'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import path from 'path'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const synthtrace = getService('logSynthtraceEsClient'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const { dashboard, header, savedObjects } = getPageObjects([ + 'dashboard', + 'header', + 'savedObjects', + ]); + + const start = moment().subtract(30, 'minutes').valueOf(); + const end = moment().add(30, 'minutes').valueOf(); + + const spaceId = 'default'; + const importFileName = 'log_stream_dashboard_saved_object.ndjson'; + const importFilePath = path.join(__dirname, 'exports', importFileName); + + describe('dashboards with log stream embeddable', () => { + before(async () => { + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(5) + .generator((timestamp: number, index: number) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset('synth.discover') + .namespace('default') + .logLevel('info') + .defaults({ + 'service.name': 'synth-discover', + }) + ), + ]); + + await savedObjects.importIntoSpace(importFilePath, spaceId); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await synthtrace.clean(); + }); + + it('should load the old log stream but with saved search embeddable', async () => { + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + + // Load saved dashboard with log stream embeddable + await dashboard.loadSavedDashboard('Logs stream dashboard test with Saves Search Embeddable'); + + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + + // Expect things from saved search embeddable to load + await testSubjects.existOrFail('unifiedDataTableToolbar'); + await testSubjects.existOrFail('dataGridHeader'); + }); + }); +} diff --git a/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson b/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson new file mode 100644 index 0000000000000..01fa518aaa2f7 --- /dev/null +++ b/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"controlGroupInput":{"chainingSystem":"HIERARCHICAL","controlStyle":"oneLine","ignoreParentSettingsJSON":"{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}","panelsJSON":"{}","showApplySelections":false},"description":"Dashboard to test Old Logs Stream Component","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"type\":\"LOG_STREAM_EMBEDDABLE\",\"title\":\"Log stream\",\"embeddableConfig\":{\"timeRange\":{\"from\":\"now-1d\",\"to\":\"now\"},\"enhancements\":{}},\"panelIndex\":\"007ae236-4c6a-4509-b3e6-0ea82a70a070\",\"gridData\":{\"i\":\"007ae236-4c6a-4509-b3e6-0ea82a70a070\",\"y\":0,\"x\":0,\"w\":29,\"h\":27}}]","timeRestore":false,"title":"Logs stream dashboard test with Saves Search Embeddable","version":3},"coreMigrationVersion":"8.8.0","created_at":"2025-06-16T11:03:19.212Z","created_by":"u_aL2aSMHLXN4K3J3Rnm1qbDWzXwMxid2fsn7Icg_S9eU_0","id":"c3448139-0e6a-4650-926c-7977a752930e","managed":false,"references":[],"type":"dashboard","typeMigrationVersion":"10.2.0","updated_at":"2025-06-16T14:23:15.285Z","updated_by":"u_aL2aSMHLXN4K3J3Rnm1qbDWzXwMxid2fsn7Icg_S9eU_0","version":"WzMxNzI5NywyM10="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} diff --git a/src/platform/test/functional/apps/discover/embeddable/index.ts b/src/platform/test/functional/apps/discover/embeddable/index.ts index 37f8d4eca7d4d..3e46cb4e9bc28 100644 --- a/src/platform/test/functional/apps/discover/embeddable/index.ts +++ b/src/platform/test/functional/apps/discover/embeddable/index.ts @@ -26,5 +26,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_saved_search_embeddable')); loadTestFile(require.resolve('./multiple_data_views')); + loadTestFile(require.resolve('./_log_stream_embeddable.ts')); }); } diff --git a/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx b/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx index 45e8080e04b13..6e9bbef978dbf 100644 --- a/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx +++ b/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx @@ -42,7 +42,7 @@ export const LogCategoryDocumentExamplesTable: React.FC diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 0ce339f16a98e..df583bb5878ab 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -11580,7 +11580,6 @@ "xpack.apm.profiling.topnFunctions.link": "Accéder aux fonctions d'Universal Profiling", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "Aucune donnée disponible", "xpack.apm.propertiesTable.agentFeature.noResultFound": "Pas de résultats pour \"{value}\".", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "Nom de service", "xpack.apm.propertiesTable.tabs.logsLabel": "Logs", "xpack.apm.propertiesTable.tabs.metadataLabel": "Métadonnées", "xpack.apm.propertiesTable.tabs.spanLinks": "Liens d'intervalle", @@ -22570,9 +22569,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "Le nombre d'alertes actives n'a pas été récupéré correctement. Essayez de recharger la page.", "xpack.infra.hostsViewPage.tabs.alerts.title": "Alertes", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "Logs de {type} \"{name}\"", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "Chargement des entrées", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "Logs par hôte", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "Rechercher les entrées de logs...", "xpack.infra.hostsViewPage.tabs.logs.title": "Logs", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Ouvrir dans Lens", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "Indicateurs", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index b0bee5043c9c9..d8285ab78c6f2 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -11564,7 +11564,6 @@ "xpack.apm.profiling.topnFunctions.link": "ユニバーサルプロファイリング機能に移動", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", "xpack.apm.propertiesTable.agentFeature.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "サービス名", "xpack.apm.propertiesTable.tabs.logsLabel": "ログ", "xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ", "xpack.apm.propertiesTable.tabs.spanLinks": "スパンリンク", @@ -22552,9 +22551,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "アクティブアラート件数が正しく取得されませんでした。ページを再読み込みしてください。", "xpack.infra.hostsViewPage.tabs.alerts.title": "アラート", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "{type} \"{name}\"からのログ", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "エントリーを読み込み中", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "ホスト別ログ", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "ログエントリーを検索...", "xpack.infra.hostsViewPage.tabs.logs.title": "ログ", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Lensで開く", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "メトリック", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index d539570ccca0c..61570201f501a 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -11588,7 +11588,6 @@ "xpack.apm.profiling.topnFunctions.link": "前往 Universal Profiling 函数", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "没有可用数据", "xpack.apm.propertiesTable.agentFeature.noResultFound": "没有“{value}”的结果。", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "服务名称", "xpack.apm.propertiesTable.tabs.logsLabel": "日志", "xpack.apm.propertiesTable.tabs.metadataLabel": "元数据", "xpack.apm.propertiesTable.tabs.spanLinks": "跨度链接", @@ -22595,9 +22594,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "未正确检索活动告警计数,请尝试重新加载页面。", "xpack.infra.hostsViewPage.tabs.alerts.title": "告警", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "来自 {type}“{name}”的日志", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "正在加载条目", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "日志(按主机)", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "搜索日志条目......", "xpack.infra.hostsViewPage.tabs.logs.title": "日志", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "在 Lens 中打开", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "指标", diff --git a/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx b/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx index aed93b8bc5921..a0462644f1288 100644 --- a/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx @@ -22,6 +22,8 @@ import type { import { CoreScopedHistory } from '@kbn/core/public'; import { coreFeatureFlagsMock } from '@kbn/core/public/mocks'; import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; @@ -74,6 +76,8 @@ export const StorybookContext: React.FC<{ optIn: () => {}, telemetryCounter$: EMPTY, }, + embeddable: {} as unknown as EmbeddableStart, + logsDataAccess: {} as unknown as LogsDataAccessPluginStart, application: getApplication(), executionContext: getExecutionContext(), featureFlags: coreFeatureFlagsMock.createStart(), diff --git a/x-pack/platform/plugins/shared/fleet/kibana.jsonc b/x-pack/platform/plugins/shared/fleet/kibana.jsonc index b51aa523e5974..e4139e517f107 100644 --- a/x-pack/platform/plugins/shared/fleet/kibana.jsonc +++ b/x-pack/platform/plugins/shared/fleet/kibana.jsonc @@ -30,6 +30,7 @@ "dashboard", "fieldsMetadata", "logsDataAccess", + "embeddable", "spaces" ], "optionalPlugins": [ @@ -56,4 +57,4 @@ "common" ] } -} \ No newline at end of file +} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx index dd1061da614ca..d3cf699887a09 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx @@ -21,11 +21,16 @@ jest.mock('@kbn/kibana-utils-plugin/public', () => { }; }); -jest.mock('@kbn/logs-shared-plugin/public', () => { - return { - LogStream: () =>
, - }; -}); +jest.mock('@kbn/saved-search-component', () => ({ + LazySavedSearchComponent: (props: any) =>
, +})); + +jest.mock('@kbn/embeddable-plugin/public', () => ({ + ViewMode: { + VIEW: 'view', + EDIT: 'edit', + }, +})); jest.mock('@kbn/logs-shared-plugin/common', () => { const originalModule = jest.requireActual('@kbn/logs-shared-plugin/common'); @@ -101,21 +106,60 @@ describe('AgentLogsUI', () => { return renderer.render(); }; - const mockStartServices = (isServerlessEnabled?: boolean) => { - mockUseStartServices.mockReturnValue({ - application: {}, - data: { - query: { - timefilter: { - timefilter: { - calculateBounds: jest.fn().mockReturnValue({ - min: '2023-10-04T13:08:53.340Z', - max: '2023-10-05T13:08:53.340Z', - }), - }, - }, + const mockLogSources = { + services: { + logSourcesService: { + getFlattenedLogSources: jest.fn().mockResolvedValue({ + id: 'logs-*', + title: 'Logs', + }), + }, + }, + }; + + const mockData = { + query: { + timefilter: { + timefilter: { + calculateBounds: jest.fn().mockReturnValue({ + min: new Date('2023-04-20T14:00:00.340Z'), + max: new Date('2023-04-20T14:20:00.340Z'), + }), }, }, + }, + search: { + searchSource: { + create: jest.fn(), + }, + }, + dataViews: { + create: jest.fn(), + }, + }; + + const mockEmbeddable = { + EmbeddablePanel: jest.fn().mockImplementation(({ children }) =>
{children}
), + }; + + const mockApplication = { + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: {}, + }, + }; + + const mockStartServices = (isServerlessEnabled?: boolean) => { + mockUseStartServices.mockImplementation(() => ({ + application: mockApplication, + data: mockData, + embeddable: mockEmbeddable, + dataViews: mockData.dataViews, + logsDataAccess: mockLogSources, + searchSource: mockData.search.searchSource, + isServerlessEnabled: isServerlessEnabled || false, share: { url: { locators: { @@ -125,7 +169,7 @@ describe('AgentLogsUI', () => { }, }, }, - }); + })); }; it('should render Open in Logs button if privileges are set', () => { diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index d092b4ce4558a..5af7d9ad61abf 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'; +import React, { memo, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, @@ -15,7 +15,6 @@ import { EuiCallOut, EuiLink, } from '@elastic/eui'; -import useMeasure from 'react-use/lib/useMeasure'; import { FormattedMessage } from '@kbn/i18n-react'; import { fromKueryExpression } from '@kbn/es-query'; import semverGte from 'semver/functions/gte'; @@ -24,12 +23,12 @@ import semverCoerce from 'semver/functions/coerce'; import { createStateContainerReactHelpers } from '@kbn/kibana-utils-plugin/public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import type { TimeRange } from '@kbn/es-query'; -import { LogStream, type LogStreamProps } from '@kbn/logs-shared-plugin/public'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; import type { Agent, AgentPolicy } from '../../../../../types'; import { useLink, useStartServices } from '../../../../../hooks'; -import { DEFAULT_DATE_RANGE } from './constants'; import { DatasetFilter } from './filter_dataset'; import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; @@ -44,19 +43,6 @@ const DatePickerFlexItem = styled(EuiFlexItem)` max-width: 312px; `; -const LOG_VIEW_SETTINGS: LogStreamProps['logView'] = { - type: 'log-view-reference', - logViewId: 'default', -}; - -const LOG_VIEW_COLUMNS: LogStreamProps['columns'] = [ - { type: 'timestamp' }, - { field: 'event.dataset', type: 'field' }, - { field: 'component.id', type: 'field' }, - { type: 'message' }, - { field: 'error.message', type: 'field' }, -]; - export interface AgentLogsProps { agent: Agent; agentPolicy?: AgentPolicy; @@ -119,13 +105,29 @@ const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: A export const AgentLogsUI: React.FunctionComponent = memo( ({ agent, agentPolicy, state }) => { - const { data, application } = useStartServices(); + const { + application, + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + data: { + search: { searchSource }, + query: { + timefilter: { timefilter: dataTimefilter }, + }, + dataViews, + }, + } = useStartServices(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) const getDateRangeTimestamps = useCallback( (timeRange: TimeRange) => { - const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); + const { min, max } = dataTimefilter.calculateBounds(timeRange); return min && max ? { start: min.valueOf(), @@ -133,7 +135,7 @@ export const AgentLogsUI: React.FunctionComponent = memo( } : undefined; }, - [data.query.timefilter.timefilter] + [dataTimefilter] ); const tryUpdateDateRange = useCallback( @@ -148,36 +150,6 @@ export const AgentLogsUI: React.FunctionComponent = memo( }, [getDateRangeTimestamps, updateState] ); - - const [dateRangeTimestamps, setDateRangeTimestamps] = useState<{ start: number; end: number }>( - getDateRangeTimestamps({ - from: state.start, - to: state.end, - }) || - getDateRangeTimestamps({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, - })! - ); - - // Attempts to parse for timestamps when start/end date expressions change - // If invalid date expressions, set expressions back to default - // Otherwise set the new timestamps - useEffect(() => { - const timestampsFromDateRange = getDateRangeTimestamps({ - from: state.start, - to: state.end, - }); - if (!timestampsFromDateRange) { - tryUpdateDateRange({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, - }); - } else { - setDateRangeTimestamps(timestampsFromDateRange); - } - }, [state.start, state.end, getDateRangeTimestamps, tryUpdateDateRange]); - // Query validation helper const isQueryValid = useCallback((testQuery: string) => { try { @@ -208,13 +180,15 @@ export const AgentLogsUI: React.FunctionComponent = memo( // Build final log stream query from agent id, datasets, log levels, and user input const logStreamQuery = useMemo( - () => - buildQuery({ + () => ({ + language: 'kuery', + query: buildQuery({ agentId: agent.id, datasets: state.datasets, logLevels: state.logLevels, userQuery: state.query, }), + }), [agent.id, state.datasets, state.logLevels, state.query] ); @@ -230,14 +204,6 @@ export const AgentLogsUI: React.FunctionComponent = memo( return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); - // Set absolute height on logs component (needed to render correctly in Safari) - // based on available height, or 600px, whichever is greater - const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); - const logPanelHeight = useMemo( - () => Math.max(measuredlogPanelHeight, 600), - [measuredlogPanelHeight] - ); - if (!isLogFeatureAvailable) { return ( = memo( }} > @@ -336,15 +302,30 @@ export const AgentLogsUI: React.FunctionComponent = memo( - - + + {logSources.value ? ( + + ) : null} diff --git a/x-pack/platform/plugins/shared/fleet/public/mock/plugin_dependencies.ts b/x-pack/platform/plugins/shared/fleet/public/mock/plugin_dependencies.ts index 250fb934493c0..a19d4322627d6 100644 --- a/x-pack/platform/plugins/shared/fleet/public/mock/plugin_dependencies.ts +++ b/x-pack/platform/plugins/shared/fleet/public/mock/plugin_dependencies.ts @@ -14,6 +14,8 @@ import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; import { customIntegrationsMock } from '@kbn/custom-integrations-plugin/public/mocks'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { logsDataAccessPluginMock } from '@kbn/logs-data-access-plugin/public/mocks'; export const createSetupDepsMock = () => { const cloud = cloudMock.createSetup(); @@ -35,5 +37,7 @@ export const createStartDepsMock = () => { customIntegrations: customIntegrationsMock.createStart(), share: sharePluginMock.createStartContract(), cloud: cloudMock.createStart(), + embeddable: embeddablePluginMock.createStartContract(), + logsDataAccess: logsDataAccessPluginMock.createStartContract(), }; }; diff --git a/x-pack/platform/plugins/shared/fleet/public/plugin.ts b/x-pack/platform/plugins/shared/fleet/public/plugin.ts index 8a9c31ccc448b..ee97afb80b5d1 100644 --- a/x-pack/platform/plugins/shared/fleet/public/plugin.ts +++ b/x-pack/platform/plugins/shared/fleet/public/plugin.ts @@ -54,6 +54,7 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { Subject } from 'rxjs'; import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/public'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { FleetAuthz } from '../common'; import { appRoutesService, INTEGRATIONS_PLUGIN_ID, PLUGIN_ID, setupRouteService } from '../common'; @@ -92,6 +93,7 @@ import type { import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; import { setCustomIntegrations, setCustomIntegrationsStart } from './services/custom_integrations'; import { getFleetDeepLinks } from './deep_links'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; export type { FleetConfigType } from '../common/types'; @@ -140,6 +142,8 @@ export interface FleetStartDeps { cloud?: CloudStart; usageCollection?: UsageCollectionStart; guidedOnboarding?: GuidedOnboardingPluginStart; + embeddable: EmbeddableStart; + logsDataAccess: LogsDataAccessPluginStart; } export interface FleetStartServices extends CoreStart, Exclude { diff --git a/x-pack/platform/plugins/shared/fleet/tsconfig.json b/x-pack/platform/plugins/shared/fleet/tsconfig.json index 7949989456e20..fb2c533cb17a2 100644 --- a/x-pack/platform/plugins/shared/fleet/tsconfig.json +++ b/x-pack/platform/plugins/shared/fleet/tsconfig.json @@ -122,5 +122,8 @@ "@kbn/core-notifications-browser-mocks", "@kbn/handlebars", "@kbn/lock-manager", + "@kbn/saved-search-component", + "@kbn/logs-data-access-plugin", + "@kbn/embeddable-plugin", ] } diff --git a/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts b/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts new file mode 100644 index 0000000000000..66b2eefc7c6b4 --- /dev/null +++ b/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * 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 { type LogsDataAccessPluginStart } from './plugin'; + +export type Start = jest.Mocked; + +const createStartContract = (): Start => { + const startContract: Start = { + services: { + logSourcesService: { + getLogSources: jest.fn(), + getFlattenedLogSources: jest.fn(), + setLogSources: jest.fn(), + }, + }, + }; + return startContract; +}; + +export const logsDataAccessPluginMock = { + createStartContract, +}; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx index d1bc98b305928..a6662e8c23445 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx @@ -93,7 +93,7 @@ export function ClassicServiceLogsStream() { index={logSources.value} timeRange={timeRange} query={query} - height={'60vh'} + height="60vh" displayOptions={{ solutionNavIdOverride: 'oblt', enableDocumentViewer: true, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index 97f21eee2d433..276a1235bd6b3 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -7,8 +7,10 @@ import { EuiSpacer, EuiTab, EuiTabs, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import { useKibana } from '../../../../context/kibana_context/use_kibana'; import type { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { TransactionMetadata } from '../../../shared/metadata_table/transaction_metadata'; import { WaterfallContainer } from './waterfall_container'; @@ -161,29 +163,55 @@ function LogsTabContent({ duration: number; traceId: string; }) { + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + }, + } = useKibana(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const startTimestamp = Math.floor(timestamp / 1000); const endTimestamp = Math.ceil(startTimestamp + duration / 1000); const framePaddingMs = 1000 * 60 * 60 * 24; // 24 hours - return ( - + + const rangeFrom = new Date(startTimestamp - framePaddingMs).toISOString(); + const rangeTo = new Date(endTimestamp + framePaddingMs).toISOString(); + + const timeRange = useMemo(() => { + return { + from: rangeFrom, + to: rangeTo, + }; + }, [rangeFrom, rangeTo]); + + const query = useMemo( + () => ({ + language: 'kuery', + query: `trace.id:"${traceId}" OR (not trace.id:* AND "${traceId}")`, + }), + [traceId] ); + + return logSources.value ? ( + + ) : null; } diff --git a/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 8423374e04fbc..46cbd8fe9475e 100644 --- a/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -7,10 +7,8 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; import { DEFAULT_LOG_VIEW, getLogsLocatorFromUrlService, @@ -20,8 +18,9 @@ import { import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; import moment from 'moment'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { InfraLoadingPanel } from '../../../loading'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDataViewsContext } from '../../hooks/use_data_views'; import { useDatePickerContext } from '../../hooks/use_date_picker'; @@ -37,13 +36,27 @@ export const Logs = () => { const { asset } = useAssetDetailsRenderPropsContext(); const { logs } = useDataViewsContext(); - const { loading: logViewLoading, reference: logViewReference } = logs ?? {}; + const { reference: logViewReference } = logs ?? {}; - const { services } = useKibanaContextForPlugin(); - const logsLocator = getLogsLocatorFromUrlService(services.share.url)!; + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + const logsLocator = getLogsLocatorFromUrlService(url)!; const [textQuery, setTextQuery] = useState(urlState?.logsSearch ?? ''); const [textQueryDebounced, setTextQueryDebounced] = useState(urlState?.logsSearch ?? ''); + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const currentTimestamp = getDateRangeInTimestamp().to; const state = useIntersectingState(ref, { currentTimestamp, @@ -132,34 +145,20 @@ export const Logs = () => { - {logViewLoading || !logViewReference ? ( - - } - /> - ) : ( - - )} + ) : null} ); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index c32b7ff9005b7..198715db813bd 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -8,9 +8,10 @@ import { EuiFlexGroup, EuiFlexItem, + EuiIcon, + EuiLink, EuiModal, EuiText, - EuiTextColor, EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -18,28 +19,53 @@ import { isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import styled from '@emotion/styled'; import type { LogEntry } from '@kbn/logs-shared-plugin/common'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; +import { getLogsLocatorFromUrlService } from '@kbn/logs-shared-plugin/common'; +import useAsync from 'react-use/lib/useAsync'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import { i18n } from '@kbn/i18n'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useDatePickerContext } from '../../../components/asset_details/hooks/use_date_picker'; import { useViewLogInProviderContext } from '../../../containers/logs/view_log_in_context'; import { useViewportDimensions } from '../../../hooks/use_viewport_dimensions'; const MODAL_MARGIN = 25; export const PageViewLogInContext: React.FC = () => { - const [{ contextEntry, startTimestamp, endTimestamp, logViewReference }, { setContextEntry }] = - useViewLogInProviderContext(); + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + + const { dateRange } = useDatePickerContext(); + const logsLocator = getLogsLocatorFromUrlService(url); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const [{ contextEntry }, { setContextEntry }] = useViewLogInProviderContext(); const closeModal = useCallback(() => setContextEntry(undefined), [setContextEntry]); const { width: vw, height: vh } = useViewportDimensions(); const contextQuery = useMemo(() => { if (contextEntry && !isEmpty(contextEntry.context)) { - return Object.entries(contextEntry.context).reduce((kuery, [key, value]) => { - const currentExpression = `${key} : "${value}"`; - if (kuery.length > 0) { - return `${kuery} AND ${currentExpression}`; - } else { - return currentExpression; - } - }, ''); + return { + language: 'kuery', + query: Object.entries(contextEntry.context).reduce((kuery, [key, value]) => { + const currentExpression = `${key} : "${value}"`; + if (kuery.length > 0) { + return `${kuery} AND ${currentExpression}`; + } else { + return currentExpression; + } + }, ''), + }; } }, [contextEntry]); @@ -47,23 +73,33 @@ export const PageViewLogInContext: React.FC = () => { return null; } + const discoverLink = logsLocator?.getRedirectUrl({ + timeRange: dateRange, + query: contextQuery, + }); + return ( - + - + - + {logSources.value ? ( + + ) : null} @@ -78,7 +114,10 @@ const LogInContextWrapper = styled.div<{ width: number | string; height: number max-height: 75vh; // Same as EuiModal `; -const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context }) => { +const LogEntryContext: React.FC<{ context: LogEntry['context']; discoverLink?: string }> = ({ + context, + discoverLink, +}) => { let text; if ('container.id' in context) { text = ( @@ -112,10 +151,36 @@ const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context } } return ( - -

- {text} -

-
+ + + +

{text}

+
+
+ {discoverLink && ( + + + + + + + + + {i18n.translate('xpack.infra.logs.viewInContext.openInDiscoverLabel', { + defaultMessage: 'Open in Discover', + })} + + + + + + )} +
); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx deleted file mode 100644 index e5edfabcb7572..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 from 'react'; -import { - getLogsLocatorFromUrlService, - type LogViewReference, -} from '@kbn/logs-shared-plugin/common'; -import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; -import moment from 'moment'; -import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; - -interface LogsLinkToStreamProps { - startTime: number; - endTime: number; - query: string; - logView: LogViewReference; -} - -export const LogsLinkToStream = ({ startTime, endTime, query, logView }: LogsLinkToStreamProps) => { - const { services } = useKibanaContextForPlugin(); - const { share } = services; - const logsLocator = getLogsLocatorFromUrlService(share.url)!; - - return ( - - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 577fcf0dfe528..fd343841a40cc 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -6,19 +6,18 @@ */ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; -import { InfraLoadingPanel } from '../../../../../../components/loading'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; +import { getLogsLocatorFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; -import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference'; import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; -import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; -import { LogsLinkToStream } from './logs_link_to_stream'; +import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; import { LogsSearchBar } from './logs_search_bar'; export const LogsTabContent = () => { @@ -28,91 +27,108 @@ export const LogsTabContent = () => { }, } = useKibanaContextForPlugin(); const isLogsOverviewEnabled = LogsOverview.useIsEnabled(); - if (isLogsOverviewEnabled) { - return ; - } else { - return ; - } + return isLogsOverviewEnabled ? : ; }; -export const LogsTabLogStreamContent = () => { - const [filterQuery] = useLogsSearchUrlState(); - const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); - const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); +export const LogsSavedSearchComponent = () => { + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + + const logsLocator = getLogsLocatorFromUrlService(url); + + const { + parsedDateRange: { from, to }, + } = useUnifiedSearchContext(); + const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo( - () => - buildCombinedAssetFilter({ - field: 'host.name', - values: hostNodes.map((p) => p.name), - }), - [hostNodes] - ); + const [filterQuery] = useLogsSearchUrlState(); - const { logViewReference: logView, loading: logViewLoading } = useLogViewReference({ - id: 'hosts-logs-view', - name: i18n.translate('xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName', { - defaultMessage: 'Logs by host', - }), - extraFields: ['host.name'], - }); + const hostsFilterQuery = useMemo(() => { + const hostsQueryPart = hostNodes.length + ? hostNodes.map((node) => `host.name: "${node.name}"`).join(' or ') + : ''; + + const urlQueryPart = filterQuery?.query ? String(filterQuery.query) : ''; - const logsLinkToStreamQuery = useMemo(() => { - const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name)); + const parts = [] as string[]; + if (hostsQueryPart) parts.push(hostsQueryPart); + if (urlQueryPart) parts.push(`(${urlQueryPart})`); - if (filterQuery.query && hostsFilterQueryParam) { - return `${filterQuery.query} and ${hostsFilterQueryParam}`; - } + return { + language: 'kuery', + query: parts.join(' and '), + }; + }, [hostNodes, filterQuery]); - return filterQuery.query || hostsFilterQueryParam; - }, [filterQuery.query, hostNodes]); + const memoizedTimeRange = useMemo(() => ({ from, to }), [from, to]); - if (loading || logViewLoading || !logView) { - return ; + const discoverLink = logsLocator?.getRedirectUrl({ + timeRange: memoizedTimeRange, + query: hostsFilterQuery, + }); + + if (!hostNodes.length && !loading) { + return ; } - return ( + return logSources.value ? ( - - - ); + ) : null; }; -const createHostsFilterQueryParam = (hostNodes: string[]): string => { - if (!hostNodes.length) { - return ''; - } - - const joinedHosts = hostNodes.join(' or '); - const hostsQueryParam = `host.name:(${joinedHosts})`; - - return hostsQueryParam; -}; +const LogsTabNoResults = () => ( + + + + + + + + + +); const LogsTabLogsOverviewContent = () => { const { @@ -146,20 +162,3 @@ const LogsTabLogsOverviewContent = () => { return ; } }; - -const LogsTabLoadingContent = () => ( - - - - } - /> - - -); diff --git a/x-pack/solutions/observability/plugins/infra/public/plugin.ts b/x-pack/solutions/observability/plugins/infra/public/plugin.ts index f120cdfafcce0..cdc621b1756d6 100644 --- a/x-pack/solutions/observability/plugins/infra/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/infra/public/plugin.ts @@ -39,7 +39,6 @@ import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; import { createMetricThresholdRuleType } from './alerting/metric_threshold'; -import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/constants'; import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; import { registerFeatures } from './register_feature'; import { InventoryViewsService } from './services/inventory_views'; @@ -184,18 +183,6 @@ export class Plugin implements InfraClientPluginClass { ) ); - pluginsSetup.embeddable.registerReactEmbeddableFactory(LOG_STREAM_EMBEDDABLE, async () => { - const { getLogStreamEmbeddableFactory } = await import( - './components/log_stream/log_stream_react_embeddable' - ); - const [coreStart, pluginDeps, pluginStart] = await core.getStartServices(); - return getLogStreamEmbeddableFactory({ - coreStart, - pluginDeps, - pluginStart, - }); - }); - pluginsSetup.observability.observabilityRuleTypeRegistry.register( createLogThresholdRuleType(core, pluginsSetup.share.url) ); diff --git a/x-pack/solutions/observability/plugins/infra/public/types.ts b/x-pack/solutions/observability/plugins/infra/public/types.ts index e8011799f635e..00faf9898a813 100644 --- a/x-pack/solutions/observability/plugins/infra/public/types.ts +++ b/x-pack/solutions/observability/plugins/infra/public/types.ts @@ -90,7 +90,7 @@ export interface InfraClientStartDeps { dataViews: DataViewsPublicPluginStart; discover: DiscoverStart; dashboard: DashboardStart; - embeddable?: EmbeddableStart; + embeddable: EmbeddableStart; lens: LensPublicStart; logsShared: LogsSharedClientStartExports; logsDataAccess: LogsDataAccessPluginStart; diff --git a/x-pack/solutions/observability/plugins/infra/tsconfig.json b/x-pack/solutions/observability/plugins/infra/tsconfig.json index 8175565596ea0..3653f26280ee0 100644 --- a/x-pack/solutions/observability/plugins/infra/tsconfig.json +++ b/x-pack/solutions/observability/plugins/infra/tsconfig.json @@ -120,6 +120,7 @@ "@kbn/presentation-containers", "@kbn/object-utils", "@kbn/coloring", + "@kbn/saved-search-component", "@kbn/core-saved-objects-server" ], "exclude": ["target/**/*"] diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 4ce52906ee439..deabc78b6c85a 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -217,6 +217,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'security', 'settings', 'header', + 'discover', ]); // Helpers @@ -633,14 +634,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should load the Logs tab section when clicking on it', async () => { - await testSubjects.existOrFail('hostsView-logs'); + await testSubjects.existOrFail('embeddedSavedSearchDocTable'); }); it('should load the Logs tab with the right columns', async () => { await retry.tryForTime(5000, async () => { - const columnLabels = await pageObjects.infraHostsView.getLogsTableColumnHeaders(); + const columnHeaders = await pageObjects.discover.getDocHeader(); - expect(columnLabels).to.eql(['Timestamp', 'host.name', 'Message']); + expect(columnHeaders).to.have.string('@timestamp'); + expect(columnHeaders).to.have.string('Summary'); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts b/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts index f6acf29cb3a9e..123de23bbe6c1 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts @@ -189,7 +189,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should load the Logs tab section when clicking on it', async () => { - await testSubjects.existOrFail('hostsView-logs'); + await testSubjects.existOrFail('embeddedSavedSearchDocTable'); }); });