[Discover / Logs] Add new "Saved Search component" (#199787)

## Summary

Implements
https://github.com/elastic/logs-dev/issues/111#issuecomment-2446470635.

This adds a new "Saved Search component". The component is a wrapper
around the current Saved Search Embeddable, but uses
`ReactEmbeddableRenderer` directly to render the embeddable outside of
Dashboard contexts. It monitors changes to things like `index`,
`filters` etc and communicates these changes through the embeddable API.

For this PoC two locations were changed to use this component 1) Logs
Overview flyout 2) APM Logs tab (when the Logs Overview isn't enabled
via advanced settings).

The component itself is technically beyond a PoC, and resides in it's
own package. ~I'd like to get eyes from the Discover folks etc on the
approach, and if we're happy I can fix the remaining known issues (apart
from the mixing of columns point as I believe this exists on the roadmap
anyway) and we can merge this for the initial two replacement points.~
[Thanks Davis
👌](https://github.com/elastic/logs-dev/issues/111#issuecomment-2475350199).

`nonPersistedDisplayOptions` is added to facilitate some configurable
options via runtime state, but without the complexity of altering the
actual saved search saved object.

On the whole I've tried to keep this as clean as possible whilst working
within the embeddable framework, outside of a dashboard context.

## Known issues

- ~"Flyout on flyout" in the logs overview flyout (e.g. triggering the
table's flyout in this context).~ Fixed with `enableFlyout` option.
- ~Filter buttons should be disabled via pills (e.g. in Summary
column).~ Fixed with `enableFilters` option.
- Summary (`_source`) column cannot be used alongside other columns,
e.g. log level, so column customisation isn't currently enabled. You'll
just get timestamp and summary. This requires changes in the Unified
Data Table. **Won't be fixed in this PR**

- We are left with this panel button that technically doesn't do
anything outside of a dashboard. I don't *think* there's an easy way to
disable this. **Won't be fixed in this PR**
![Screenshot 2024-11-20 at 11 50
43](https://github.com/user-attachments/assets/e43a47cd-e36e-4511-ba88-c928a4acd634)


## Followups

- ~The Logs Overview details state machine can be cleaned up (it doesn't
need to fetch documents etc anymore).~ The state machine no longer
fetches it's own documents. Some scaffolding is left in place as it'll
be needed for showing category details anyway.

## Example

![Screenshot 2024-11-20 at 12 20
08](https://github.com/user-attachments/assets/3b25d591-e3e2-4e8a-98a8-1bfc849d3bc1)
![Screenshot 2024-11-20 at 12 23
34](https://github.com/user-attachments/assets/a2d28036-98c5-4404-934e-2298cf4a66bf)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kerry Gallagher 2024-11-29 23:09:24 +00:00 committed by GitHub
parent 50a2ffa7f2
commit b0122f547d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 637 additions and 477 deletions

1
.github/CODEOWNERS vendored
View file

@ -467,6 +467,7 @@ packages/kbn-rrule @elastic/response-ops
packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team
packages/kbn-safer-lodash-set @elastic/kibana-security
packages/kbn-saved-objects-settings @elastic/appex-sharedux
packages/kbn-saved-search-component @elastic/obs-ux-logs-team
packages/kbn-scout @elastic/appex-qa
packages/kbn-screenshotting-server @elastic/appex-sharedux
packages/kbn-search-api-keys-components @elastic/search-kibana

View file

@ -785,6 +785,7 @@
"@kbn/saved-objects-settings": "link:packages/kbn-saved-objects-settings",
"@kbn/saved-objects-tagging-oss-plugin": "link:src/plugins/saved_objects_tagging_oss",
"@kbn/saved-objects-tagging-plugin": "link:x-pack/plugins/saved_objects_tagging",
"@kbn/saved-search-component": "link:packages/kbn-saved-search-component",
"@kbn/saved-search-plugin": "link:src/plugins/saved_search",
"@kbn/screenshot-mode-example-plugin": "link:examples/screenshot_mode_example",
"@kbn/screenshot-mode-plugin": "link:src/plugins/screenshot_mode",

View file

@ -0,0 +1,26 @@
# @kbn/saved-search-component
A component wrapper around Discover's Saved Search embeddable. This can be used in solutions without being within a Dasboard context.
This can be used to render a context-aware (logs etc) "document table".
In the past you may have used the Log Stream Component to achieve this, this component supersedes that.
## Basic usage
```
import { LazySavedSearchComponent } from '@kbn/saved-search-component';
<LazySavedSearchComponent
dependencies={{
embeddable: dependencies.embeddable,
savedSearch: dependencies.savedSearch,
dataViews: dependencies.dataViews,
searchSource: dependencies.searchSource,
}}
index={anIndexString}
filters={optionalFilters}
query={optionalQuery}
timestampField={optionalTimestampFieldString}
/>
```

View file

@ -0,0 +1,18 @@
/*
* 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 { dynamic } from '@kbn/shared-ux-utility';
export type { SavedSearchComponentDependencies, SavedSearchComponentProps } from './src/types';
export const LazySavedSearchComponent = dynamic(() =>
import('./src/components/saved_search').then((mod) => ({
default: mod.SavedSearchComponent,
}))
);

View file

@ -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", 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".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-saved-search-component'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/saved-search-component",
"owner": "@elastic/obs-ux-logs-team"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/saved-search-component",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"sideEffects": false
}

View file

@ -0,0 +1,42 @@
/*
* 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
export interface SavedSearchComponentErrorContentProps {
error?: Error;
}
export const SavedSearchComponentErrorContent: React.FC<SavedSearchComponentErrorContentProps> = ({
error,
}) => {
return (
<EuiEmptyPrompt
color="danger"
iconType="error"
title={<h2>{SavedSearchComponentErrorTitle}</h2>}
body={
<EuiCodeBlock className="eui-textLeft" whiteSpace="pre">
<p>{error?.stack ?? error?.toString() ?? unknownErrorDescription}</p>
</EuiCodeBlock>
}
layout="vertical"
/>
);
};
const SavedSearchComponentErrorTitle = i18n.translate('savedSearchComponent.errorTitle', {
defaultMessage: 'Error',
});
const unknownErrorDescription = i18n.translate('savedSearchComponent.unknownErrorDescription', {
defaultMessage: 'An unspecified error occurred.',
});

View file

@ -0,0 +1,214 @@
/*
* 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 React, { useEffect, useMemo, useRef, useState } from 'react';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
import type {
SearchEmbeddableSerializedState,
SearchEmbeddableRuntimeState,
SearchEmbeddableApi,
} from '@kbn/discover-plugin/public';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { css } from '@emotion/react';
import { SavedSearchComponentProps } from '../types';
import { SavedSearchComponentErrorContent } from './error';
const TIMESTAMP_FIELD = '@timestamp';
export const SavedSearchComponent: React.FC<SavedSearchComponentProps> = (props) => {
// Creates our *initial* search source and set of attributes.
// Future changes to these properties will be facilitated by the Parent API from the embeddable.
const [initialSerializedState, setInitialSerializedState] =
useState<SerializedPanelState<SearchEmbeddableSerializedState>>();
const [error, setError] = useState<Error | undefined>();
const {
dependencies: { dataViews, searchSource: searchSourceService },
timeRange,
query,
filters,
index,
timestampField,
height,
} = props;
const {
enableDocumentViewer: documentViewerEnabled = true,
enableFilters: filtersEnabled = true,
} = props.displayOptions ?? {};
useEffect(() => {
// Ensure we get a stabilised set of initial state incase dependencies change, as
// the data view creation process is async.
const abortController = new AbortController();
async function createInitialSerializedState() {
try {
// Ad-hoc data view
const dataView = await dataViews.create({
title: index,
timeFieldName: timestampField ?? TIMESTAMP_FIELD,
});
if (!abortController.signal.aborted) {
// Search source
const searchSource = searchSourceService.createEmpty();
searchSource.setField('index', dataView);
searchSource.setField('query', query);
searchSource.setField('filter', filters);
const { searchSourceJSON, references } = searchSource.serialize();
// By-value saved object structure
const attributes = {
kibanaSavedObjectMeta: {
searchSourceJSON,
},
};
setInitialSerializedState({
rawState: {
attributes: { ...attributes, references },
timeRange,
nonPersistedDisplayOptions: {
enableDocumentViewer: documentViewerEnabled,
enableFilters: filtersEnabled,
},
} as SearchEmbeddableSerializedState,
references,
});
}
} catch (e) {
setError(e);
}
}
createInitialSerializedState();
return () => {
abortController.abort();
};
}, [
dataViews,
documentViewerEnabled,
filters,
filtersEnabled,
index,
query,
searchSourceService,
timeRange,
timestampField,
]);
if (error) {
return <SavedSearchComponentErrorContent error={error} />;
}
return initialSerializedState ? (
<div
css={css`
height: ${height ?? '100%'};
> [data-test-subj='embeddedSavedSearchDocTable'] {
height: 100%;
}
`}
>
<SavedSearchComponentTable {...props} initialSerializedState={initialSerializedState} />
</div>
) : null;
};
const SavedSearchComponentTable: React.FC<
SavedSearchComponentProps & {
initialSerializedState: SerializedPanelState<SearchEmbeddableSerializedState>;
}
> = (props) => {
const {
dependencies: { dataViews },
initialSerializedState,
filters,
query,
timeRange,
timestampField,
index,
} = props;
const embeddableApi = useRef<SearchEmbeddableApi | undefined>(undefined);
const parentApi = useMemo(() => {
return {
getSerializedStateForChild: () => {
return initialSerializedState;
},
};
}, [initialSerializedState]);
useEffect(
function syncIndex() {
if (!embeddableApi.current) return;
const abortController = new AbortController();
async function updateDataView() {
// Ad-hoc data view
const dataView = await dataViews.create({
title: index,
timeFieldName: timestampField ?? TIMESTAMP_FIELD,
});
if (!abortController.signal.aborted) {
embeddableApi.current?.setDataViews([dataView]);
}
}
updateDataView();
return () => {
abortController.abort();
};
},
[dataViews, index, timestampField]
);
useEffect(
function syncFilters() {
if (!embeddableApi.current) return;
embeddableApi.current.setFilters(filters);
},
[filters]
);
useEffect(
function syncQuery() {
if (!embeddableApi.current) return;
embeddableApi.current.setQuery(query);
},
[query]
);
useEffect(
function syncTimeRange() {
if (!embeddableApi.current) return;
embeddableApi.current.setTimeRange(timeRange);
},
[timeRange]
);
return (
<ReactEmbeddableRenderer<
SearchEmbeddableSerializedState,
SearchEmbeddableRuntimeState,
SearchEmbeddableApi
>
maybeId={undefined}
type={SEARCH_EMBEDDABLE_TYPE}
getParentApi={() => parentApi}
onApiAvailable={(api) => {
embeddableApi.current = api;
}}
hidePanelChrome
/>
);
};

View file

@ -0,0 +1,31 @@
/*
* 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 { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { DataViewsContract, ISearchStartSearchSource } from '@kbn/data-plugin/public';
import type { NonPersistedDisplayOptions } from '@kbn/discover-plugin/public';
import { CSSProperties } from 'react';
export interface SavedSearchComponentDependencies {
embeddable: EmbeddableStart;
searchSource: ISearchStartSearchSource;
dataViews: DataViewsContract;
}
export interface SavedSearchComponentProps {
dependencies: SavedSearchComponentDependencies;
index: string;
timeRange?: TimeRange;
query?: Query;
filters?: Filter[];
timestampField?: string;
height?: CSSProperties['height'];
displayOptions?: NonPersistedDisplayOptions;
}

View file

@ -0,0 +1,29 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop",
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/embeddable-plugin",
"@kbn/shared-ux-utility",
"@kbn/discover-utils",
"@kbn/es-query",
"@kbn/data-plugin",
"@kbn/discover-plugin",
"@kbn/presentation-containers",
"@kbn/i18n",
]
}

View file

@ -97,7 +97,11 @@ export {
apiPublishesDataLoading,
type PublishesDataLoading,
} from './interfaces/publishes_data_loading';
export { apiPublishesDataViews, type PublishesDataViews } from './interfaces/publishes_data_views';
export {
apiPublishesDataViews,
type PublishesDataViews,
type PublishesWritableDataViews,
} from './interfaces/publishes_data_views';
export {
apiPublishesDisabledActionIds,
type PublishesDisabledActionIds,

View file

@ -14,6 +14,10 @@ export interface PublishesDataViews {
dataViews: PublishingSubject<DataView[] | undefined>;
}
export type PublishesWritableDataViews = PublishesDataViews & {
setDataViews: (dataViews: DataView[]) => void;
};
export const apiPublishesDataViews = (
unknownApi: null | unknown
): unknownApi is PublishesDataViews => {

View file

@ -36,12 +36,13 @@ interface DiscoverGridEmbeddableProps extends Omit<UnifiedDataTableProps, 'sampl
onAddColumn: (column: string) => void;
onRemoveColumn: (column: string) => void;
savedSearchId?: string;
enableDocumentViewer: boolean;
}
export const DiscoverGridMemoized = React.memo(DiscoverGrid);
export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
const { interceptedWarnings, ...gridProps } = props;
const { interceptedWarnings, enableDocumentViewer, ...gridProps } = props;
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
@ -131,7 +132,7 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
expandedDoc={expandedDoc}
showMultiFields={props.services.uiSettings.get(SHOW_MULTIFIELDS)}
maxDocFieldsDisplayed={props.services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
renderDocumentView={renderDocumentView}
renderDocumentView={enableDocumentViewer ? renderDocumentView : undefined}
renderCustomToolbar={renderCustomToolbarWithElements}
externalCustomRenderers={cellRenderers}
enableComparisonMode

View file

@ -49,6 +49,7 @@ interface SavedSearchEmbeddableComponentProps {
};
dataView: DataView;
onAddFilter?: DocViewFilterFn;
enableDocumentViewer: boolean;
stateManager: SearchEmbeddableStateManager;
}
@ -59,6 +60,7 @@ export function SearchEmbeddableGridComponent({
api,
dataView,
onAddFilter,
enableDocumentViewer,
stateManager,
}: SavedSearchEmbeddableComponentProps) {
const discoverServices = useDiscoverServices();
@ -272,6 +274,7 @@ export function SearchEmbeddableGridComponent({
services={discoverServices}
showTimeCol={!discoverServices.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false)}
dataGridDensityState={savedSearch.density}
enableDocumentViewer={enableDocumentViewer}
/>
);
}

View file

@ -37,6 +37,7 @@ import { initializeEditApi } from './initialize_edit_api';
import { initializeFetch, isEsqlMode } from './initialize_fetch';
import { initializeSearchEmbeddableApi } from './initialize_search_embeddable_api';
import {
NonPersistedDisplayOptions,
SearchEmbeddableApi,
SearchEmbeddableRuntimeState,
SearchEmbeddableSerializedState,
@ -84,6 +85,11 @@ export const getSearchEmbeddableFactory = ({
initialState?.savedObjectDescription
);
/** By-value SavedSearchComponent package (non-dashboard contexts) state, to adhere to the comparator contract of an embeddable. */
const nonPersistedDisplayOptions$ = new BehaviorSubject<
NonPersistedDisplayOptions | undefined
>(initialState?.nonPersistedDisplayOptions);
/** All other state */
const blockingError$ = new BehaviorSubject<Error | undefined>(undefined);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(true);
@ -201,6 +207,10 @@ export const getSearchEmbeddableFactory = ({
defaultPanelDescription$,
(value) => defaultPanelDescription$.next(value),
],
nonPersistedDisplayOptions: [
nonPersistedDisplayOptions$,
(value) => nonPersistedDisplayOptions$.next(value),
],
}
);
@ -304,7 +314,18 @@ export const getSearchEmbeddableFactory = ({
<SearchEmbeddableGridComponent
api={{ ...api, fetchWarnings$, fetchContext$ }}
dataView={dataView!}
onAddFilter={isEsqlMode(savedSearch) ? undefined : onAddFilter}
onAddFilter={
isEsqlMode(savedSearch) ||
initialState.nonPersistedDisplayOptions?.enableFilters === false
? undefined
: onAddFilter
}
enableDocumentViewer={
initialState.nonPersistedDisplayOptions?.enableDocumentViewer !==
undefined
? initialState.nonPersistedDisplayOptions?.enableDocumentViewer
: true
}
stateManager={searchEmbeddable.stateManager}
/>
</CellActionsProvider>

View file

@ -15,8 +15,8 @@ import { ISearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/co
import { DataView } from '@kbn/data-views-plugin/common';
import { DataTableRecord } from '@kbn/discover-utils/types';
import type {
PublishesDataViews,
PublishesUnifiedSearch,
PublishesWritableUnifiedSearch,
PublishesWritableDataViews,
StateComparators,
} from '@kbn/presentation-publishing';
import { DiscoverGridSettings, SavedSearch } from '@kbn/saved-search-plugin/common';
@ -71,7 +71,7 @@ export const initializeSearchEmbeddableApi = async (
discoverServices: DiscoverServices;
}
): Promise<{
api: PublishesSavedSearch & PublishesDataViews & Partial<PublishesUnifiedSearch>;
api: PublishesSavedSearch & PublishesWritableDataViews & Partial<PublishesWritableUnifiedSearch>;
stateManager: SearchEmbeddableStateManager;
comparators: StateComparators<SearchEmbeddableSerializedAttributes>;
cleanup: () => void;
@ -144,6 +144,25 @@ export const initializeSearchEmbeddableApi = async (
pick(stateManager, EDITABLE_SAVED_SEARCH_KEYS)
);
/** APIs for updating search source properties */
const setDataViews = (nextDataViews: DataView[]) => {
searchSource.setField('index', nextDataViews[0]);
dataViews.next(nextDataViews);
searchSource$.next(searchSource);
};
const setFilters = (filters: Filter[] | undefined) => {
searchSource.setField('filter', filters);
filters$.next(filters);
searchSource$.next(searchSource);
};
const setQuery = (query: Query | AggregateQuery | undefined) => {
searchSource.setField('query', query);
query$.next(query);
searchSource$.next(searchSource);
};
/** Keep the saved search in sync with any state changes */
const syncSavedSearch = combineLatest([onAnyStateChange, searchSource$])
.pipe(
@ -163,10 +182,13 @@ export const initializeSearchEmbeddableApi = async (
syncSavedSearch.unsubscribe();
},
api: {
setDataViews,
dataViews,
savedSearch$,
filters$,
setFilters,
query$,
setQuery,
},
stateManager,
comparators: {

View file

@ -15,10 +15,9 @@ import {
HasInPlaceLibraryTransforms,
PublishesBlockingError,
PublishesDataLoading,
PublishesDataViews,
PublishesSavedObjectId,
PublishesUnifiedSearch,
PublishesWritablePanelTitle,
PublishesWritableUnifiedSearch,
PublishingSubject,
SerializedTimeRange,
SerializedTitles,
@ -30,6 +29,7 @@ import {
} from '@kbn/saved-search-plugin/common/types';
import { DataTableColumnsMeta } from '@kbn/unified-data-table';
import { BehaviorSubject } from 'rxjs';
import { PublishesWritableDataViews } from '@kbn/presentation-publishing/interfaces/publishes_data_views';
import { EDITABLE_SAVED_SEARCH_KEYS } from './constants';
export type SearchEmbeddableState = Pick<
@ -59,6 +59,13 @@ export type SearchEmbeddableSerializedAttributes = Omit<
> &
Pick<SerializableSavedSearch, 'serializedSearchSource'>;
// These are options that are not persisted in the saved object, but can be used by solutions
// when utilising the SavedSearchComponent package outside of dashboard contexts.
export interface NonPersistedDisplayOptions {
enableDocumentViewer?: boolean;
enableFilters?: boolean;
}
export type SearchEmbeddableSerializedState = SerializedTitles &
SerializedTimeRange &
Partial<Pick<SavedSearchAttributes, (typeof EDITABLE_SAVED_SEARCH_KEYS)[number]>> & {
@ -66,6 +73,7 @@ export type SearchEmbeddableSerializedState = SerializedTitles &
attributes?: SavedSearchAttributes & { references: SavedSearch['references'] };
// by reference
savedObjectId?: string;
nonPersistedDisplayOptions?: NonPersistedDisplayOptions;
};
export type SearchEmbeddableRuntimeState = SearchEmbeddableSerializedAttributes &
@ -74,20 +82,20 @@ export type SearchEmbeddableRuntimeState = SearchEmbeddableSerializedAttributes
savedObjectTitle?: string;
savedObjectId?: string;
savedObjectDescription?: string;
nonPersistedDisplayOptions?: NonPersistedDisplayOptions;
};
export type SearchEmbeddableApi = DefaultEmbeddableApi<
SearchEmbeddableSerializedState,
SearchEmbeddableRuntimeState
> &
PublishesDataViews &
PublishesSavedObjectId &
PublishesDataLoading &
PublishesBlockingError &
PublishesWritablePanelTitle &
PublishesSavedSearch &
PublishesDataViews &
PublishesUnifiedSearch &
PublishesWritableDataViews &
PublishesWritableUnifiedSearch &
HasInPlaceLibraryTransforms &
HasTimeRange &
Partial<HasEditCapabilities & PublishesSavedObjectId>;

View file

@ -67,6 +67,7 @@ export const deserializeState = async ({
return {
...savedSearch,
...panelState,
nonPersistedDisplayOptions: serializedState.rawState.nonPersistedDisplayOptions,
};
}
};

View file

@ -35,6 +35,9 @@ export {
type PublishesSavedSearch,
type HasTimeRange,
type SearchEmbeddableSerializedState,
type SearchEmbeddableRuntimeState,
type SearchEmbeddableApi,
type NonPersistedDisplayOptions,
} from './embeddable';
export { loadSharingDataHelpers } from './utils';
export { LogsExplorerTabs, type LogsExplorerTabsProps } from './components/logs_explorer_tabs';

View file

@ -1534,6 +1534,8 @@
"@kbn/saved-objects-tagging-oss-plugin/*": ["src/plugins/saved_objects_tagging_oss/*"],
"@kbn/saved-objects-tagging-plugin": ["x-pack/plugins/saved_objects_tagging"],
"@kbn/saved-objects-tagging-plugin/*": ["x-pack/plugins/saved_objects_tagging/*"],
"@kbn/saved-search-component": ["packages/kbn-saved-search-component"],
"@kbn/saved-search-component/*": ["packages/kbn-saved-search-component/*"],
"@kbn/saved-search-plugin": ["src/plugins/saved_search"],
"@kbn/saved-search-plugin/*": ["src/plugins/saved_search/*"],
"@kbn/scout": ["packages/kbn-scout"],

View file

@ -76,7 +76,6 @@ export const LogCategoriesResultContent: React.FC<LogCategoriesResultContentProp
<LogCategoryDetailsFlyout
logCategory={categoryDetailsServiceState.context.expandedCategory}
onCloseFlyout={onCloseFlyout}
categoryDetailsServiceState={categoryDetailsServiceState}
logsSource={logsSource}
dependencies={dependencies}
documentFilters={documentFilters}

View file

@ -17,28 +17,32 @@ import {
} from '@elastic/eui';
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { StateFrom } from 'xstate5';
import { i18n } from '@kbn/i18n';
import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { css } from '@emotion/react';
import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
import { LogCategory } from '../../types';
import { LogCategoryPattern } from '../shared/log_category_pattern';
import { categoryDetailsService } from '../../services/category_details_service';
import {
LogCategoryDocumentExamplesTable,
LogCategoryDocumentExamplesTableDependencies,
} from './log_category_document_examples_table';
import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import { LogCategoryDetailsLoadingContent } from './log_category_details_loading_content';
import { LogCategoryDetailsErrorContent } from './log_category_details_error_content';
import { DiscoverLink } from '../discover_link';
import { DiscoverLink, DiscoverLinkDependencies } from '../discover_link';
import { createCategoryQuery } from '../../services/categorize_logs_service/queries';
export type LogCategoriesFlyoutDependencies = LogCategoryDocumentExamplesTableDependencies;
export type LogCategoriesFlyoutDependencies = LogCategoryDocumentExamplesTableDependencies &
DiscoverLinkDependencies;
const flyoutBodyCss = css`
.euiFlyoutBody__overflowContent {
height: 100%;
}
`;
interface LogCategoryDetailsFlyoutProps {
onCloseFlyout: () => void;
logCategory: LogCategory;
categoryDetailsServiceState: StateFrom<typeof categoryDetailsService>;
dependencies: LogCategoriesFlyoutDependencies;
logsSource: ResolvedIndexNameLogsSourceConfiguration;
documentFilters?: QueryDslQueryContainer[];
@ -51,7 +55,6 @@ interface LogCategoryDetailsFlyoutProps {
export const LogCategoryDetailsFlyout: React.FC<LogCategoryDetailsFlyoutProps> = ({
onCloseFlyout,
logCategory,
categoryDetailsServiceState,
dependencies,
logsSource,
documentFilters,
@ -61,11 +64,19 @@ export const LogCategoryDetailsFlyout: React.FC<LogCategoryDetailsFlyoutProps> =
prefix: 'flyoutTitle',
});
const categoryFilter = useMemo(() => {
return createCategoryQuery(logsSource.messageField)(logCategory.terms);
}, [logCategory.terms, logsSource.messageField]);
const documentAndCategoryFilters = useMemo(() => {
return [...(documentFilters ?? []), categoryFilter];
}, [categoryFilter, documentFilters]);
const linkFilters = useMemo(() => {
return [
...(documentFilters ? documentFilters.map((filter) => ({ filter })) : []),
{
filter: createCategoryQuery(logsSource.messageField)(logCategory.terms),
filter: categoryFilter,
meta: {
name: i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.discoverLinkFilterName',
@ -79,7 +90,20 @@ export const LogCategoryDetailsFlyout: React.FC<LogCategoryDetailsFlyoutProps> =
},
},
];
}, [documentFilters, logCategory.terms, logsSource.messageField]);
}, [categoryFilter, documentFilters, logCategory.terms]);
const filters = useMemo(() => {
return documentAndCategoryFilters.map((filter) =>
buildCustomFilter(
logsSource.indexName,
filter,
false,
false,
'Document filters',
FilterStateStore.APP_STATE
)
);
}, [documentAndCategoryFilters, logsSource.indexName]);
return (
<EuiFlyout ownFocus onClose={() => onCloseFlyout()} aria-labelledby={flyoutTitleId}>
@ -107,32 +131,12 @@ export const LogCategoryDetailsFlyout: React.FC<LogCategoryDetailsFlyoutProps> =
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{categoryDetailsServiceState.matches({ hasCategory: 'fetchingDocuments' }) ? (
<LogCategoryDetailsLoadingContent
message={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.loadingMessage',
{
defaultMessage: 'Loading latest documents',
}
)}
/>
) : categoryDetailsServiceState.matches({ hasCategory: 'error' }) ? (
<LogCategoryDetailsErrorContent
title={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.fetchingDocumentsErrorTitle',
{
defaultMessage: 'Failed to fetch documents',
}
)}
/>
) : (
<LogCategoryDocumentExamplesTable
dependencies={dependencies}
categoryDocuments={categoryDetailsServiceState.context.categoryDocuments}
logsSource={logsSource}
/>
)}
<EuiFlyoutBody css={flyoutBodyCss}>
<LogCategoryDocumentExamplesTable
dependencies={dependencies}
logsSource={logsSource}
filters={filters}
/>
</EuiFlyoutBody>
</EuiFlyout>
);

View file

@ -5,147 +5,45 @@
* 2.0.
*/
import { EuiBasicTable, EuiBasicTableColumn, EuiSpacer, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
import moment from 'moment';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { getLogLevelBadgeCell, LazySummaryColumn } from '@kbn/discover-contextual-components';
import type { LogCategoryDocument } from '../../services/category_details_service/types';
import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import React from 'react';
import { LazySavedSearchComponent } from '@kbn/saved-search-component';
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { Filter } from '@kbn/es-query';
import { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
export interface LogCategoryDocumentExamplesTableDependencies {
core: CoreStart;
uiSettings: SettingsStart;
fieldFormats: FieldFormatsStart;
share: SharePluginStart;
embeddable: EmbeddableStart;
dataViews: DataViewsContract;
searchSource: ISearchStartSearchSource;
}
export interface LogCategoryDocumentExamplesTableProps {
dependencies: LogCategoryDocumentExamplesTableDependencies;
categoryDocuments: LogCategoryDocument[];
logsSource: ResolvedIndexNameLogsSourceConfiguration;
filters: Filter[];
}
const TimestampCell = ({
dependencies,
timestamp,
}: {
dependencies: LogCategoryDocumentExamplesTableDependencies;
timestamp?: string | number;
}) => {
const dateFormat = useMemo(
() => dependencies.uiSettings.client.get('dateFormat'),
[dependencies.uiSettings.client]
);
if (!timestamp) return null;
if (dateFormat) {
return <>{moment(timestamp).format(dateFormat)}</>;
} else {
return <>{timestamp}</>;
}
};
const LogLevelBadgeCell = getLogLevelBadgeCell('log.level');
export const LogCategoryDocumentExamplesTable: React.FC<LogCategoryDocumentExamplesTableProps> = ({
categoryDocuments,
dependencies,
filters,
logsSource,
}) => {
const columns: Array<EuiBasicTableColumn<LogCategoryDocument>> = [
{
field: 'row',
name: 'Timestamp',
width: '25%',
render: (row: any) => {
return (
<TimestampCell
dependencies={dependencies}
timestamp={row.raw[logsSource.timestampField]}
/>
);
},
},
{
field: 'row',
name: 'Log level',
width: '10%',
render: (row: any) => {
return (
<LogLevelBadgeCell
rowIndex={0}
colIndex={0}
columnId="row"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={row}
dataView={logsSource.dataView}
fieldFormats={dependencies.fieldFormats}
setCellProps={() => {}}
closePopover={() => {}}
/>
);
},
},
{
field: 'row',
name: 'Summary',
width: '65%',
render: (row: any) => {
return (
<LazySummaryColumn
rowIndex={0}
colIndex={0}
columnId="_source"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={row}
dataView={logsSource.dataView}
fieldFormats={dependencies.fieldFormats}
setCellProps={() => {}}
closePopover={() => {}}
density={DataGridDensity.COMPACT}
rowHeight={ROWS_HEIGHT_OPTIONS.single}
shouldShowFieldHandler={() => false}
core={dependencies.core}
share={dependencies.share}
/>
);
},
},
];
return (
<>
<EuiText size="xs" color="subdued">
{i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDocumentExamplesTable.documentCountText',
{
defaultMessage: 'Displaying the latest {documentsCount} documents.',
values: {
documentsCount: categoryDocuments.length,
},
}
)}
</EuiText>
<EuiSpacer size="s" />
<EuiBasicTable
tableCaption={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDocumentExamplesTable.tableCaption',
{
defaultMessage: 'Log category example documents table',
}
)}
items={categoryDocuments}
columns={columns}
/>
</>
<LazySavedSearchComponent
dependencies={{
embeddable: dependencies.embeddable,
dataViews: dependencies.dataViews,
searchSource: dependencies.searchSource,
}}
filters={filters}
index={logsSource.indexName}
timestampField={logsSource.timestampField}
displayOptions={{
enableDocumentViewer: false,
enableFilters: false,
}}
/>
);
};

View file

@ -7,40 +7,24 @@
import { MachineImplementationsFrom, assign, setup } from 'xstate5';
import { LogCategory } from '../../types';
import { getPlaceholderFor } from '../../utils/xstate5_utils';
import {
CategoryDetailsServiceDependencies,
LogCategoryDocument,
LogCategoryDetailsParams,
} from './types';
import { getCategoryDocuments } from './category_documents';
import { CategoryDetailsServiceDependencies, LogCategoryDetailsParams } from './types';
export const categoryDetailsService = setup({
types: {
input: {} as LogCategoryDetailsParams,
output: {} as {
categoryDocuments: LogCategoryDocument[] | null;
},
output: {} as {},
context: {} as {
parameters: LogCategoryDetailsParams;
error?: Error;
expandedRowIndex: number | null;
expandedCategory: LogCategory | null;
categoryDocuments: LogCategoryDocument[];
},
events: {} as
| {
type: 'cancel';
}
| {
type: 'setExpandedCategory';
rowIndex: number | null;
category: LogCategory | null;
},
},
actors: {
getCategoryDocuments: getPlaceholderFor(getCategoryDocuments),
events: {} as {
type: 'setExpandedCategory';
rowIndex: number | null;
category: LogCategory | null;
},
},
actors: {},
actions: {
storeCategory: assign(
({ context, event }, params: { category: LogCategory | null; rowIndex: number | null }) => ({
@ -48,22 +32,10 @@ export const categoryDetailsService = setup({
expandedRowIndex: params.rowIndex,
})
),
storeDocuments: assign(
({ context, event }, params: { categoryDocuments: LogCategoryDocument[] }) => ({
categoryDocuments: params.categoryDocuments,
})
),
storeError: assign((_, params: { error: unknown }) => ({
error: params.error instanceof Error ? params.error : new Error(String(params.error)),
})),
},
guards: {
hasCategory: (_guardArgs, params: { expandedCategory: LogCategory | null }) =>
params.expandedCategory !== null,
hasDocumentExamples: (
_guardArgs,
params: { categoryDocuments: LogCategoryDocument[] | null }
) => params.categoryDocuments !== null && params.categoryDocuments.length > 0,
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
@ -71,7 +43,6 @@ export const categoryDetailsService = setup({
context: ({ input }) => ({
expandedCategory: null,
expandedRowIndex: null,
categoryDocuments: [],
parameters: input,
}),
initial: 'idle',
@ -79,7 +50,6 @@ export const categoryDetailsService = setup({
idle: {
on: {
setExpandedCategory: {
target: 'checkingCategoryState',
actions: [
{
type: 'storeCategory',
@ -89,103 +59,12 @@ export const categoryDetailsService = setup({
},
},
},
checkingCategoryState: {
always: [
{
guard: {
type: 'hasCategory',
params: ({ event, context }) => {
return {
expandedCategory: context.expandedCategory,
};
},
},
target: '#hasCategory.fetchingDocuments',
},
{ target: 'idle' },
],
},
hasCategory: {
id: 'hasCategory',
initial: 'fetchingDocuments',
on: {
setExpandedCategory: {
target: 'checkingCategoryState',
actions: [
{
type: 'storeCategory',
params: ({ event }) => event,
},
],
},
},
states: {
fetchingDocuments: {
invoke: {
src: 'getCategoryDocuments',
id: 'fetchCategoryDocumentExamples',
input: ({ context }) => ({
...context.parameters,
categoryTerms: context.expandedCategory!.terms,
}),
onDone: [
{
guard: {
type: 'hasDocumentExamples',
params: ({ event }) => {
return event.output;
},
},
target: 'hasData',
actions: [
{
type: 'storeDocuments',
params: ({ event }) => {
return event.output;
},
},
],
},
{
target: 'noData',
actions: [
{
type: 'storeDocuments',
params: ({ event }) => {
return { categoryDocuments: [] };
},
},
],
},
],
onError: {
target: 'error',
actions: [
{
type: 'storeError',
params: ({ event }) => ({ error: event.error }),
},
],
},
},
},
hasData: {},
noData: {},
error: {},
},
},
},
output: ({ context }) => ({
categoryDocuments: context.categoryDocuments,
}),
output: ({ context }) => ({}),
});
export const createCategoryDetailsServiceImplementations = ({
search,
}: CategoryDetailsServiceDependencies): MachineImplementationsFrom<
typeof categoryDetailsService
> => ({
actors: {
getCategoryDocuments: getCategoryDocuments({ search }),
},
});
> => ({});

View file

@ -1,63 +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 { ISearchGeneric } from '@kbn/search-types';
import { fromPromise } from 'xstate5';
import { lastValueFrom } from 'rxjs';
import { flattenHit } from '@kbn/data-service';
import { LogCategoryDocument, LogCategoryDocumentsParams } from './types';
import { createGetLogCategoryDocumentsRequestParams } from './queries';
export const getCategoryDocuments = ({ search }: { search: ISearchGeneric }) =>
fromPromise<
{
categoryDocuments: LogCategoryDocument[];
},
LogCategoryDocumentsParams
>(
async ({
input: {
index,
endTimestamp,
startTimestamp,
timeField,
messageField,
categoryTerms,
additionalFilters = [],
dataView,
},
signal,
}) => {
const requestParams = createGetLogCategoryDocumentsRequestParams({
index,
timeField,
messageField,
startTimestamp,
endTimestamp,
additionalFilters,
categoryTerms,
});
const { rawResponse } = await lastValueFrom(
search({ params: requestParams }, { abortSignal: signal })
);
const categoryDocuments: LogCategoryDocument[] =
rawResponse.hits?.hits.map((hit) => {
return {
row: {
raw: hit._source,
flattened: flattenHit(hit, dataView),
},
};
}) ?? [];
return {
categoryDocuments,
};
}
);

View file

@ -1,58 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { createCategoryQuery } from '../categorize_logs_service/queries';
export const createGetLogCategoryDocumentsRequestParams = ({
index,
timeField,
messageField,
startTimestamp,
endTimestamp,
additionalFilters = [],
categoryTerms = '',
documentCount = 20,
}: {
startTimestamp: string;
endTimestamp: string;
index: string;
timeField: string;
messageField: string;
additionalFilters?: QueryDslQueryContainer[];
categoryTerms?: string;
documentCount?: number;
}) => {
return {
index,
size: documentCount,
track_total_hits: false,
sort: [{ [timeField]: { order: 'desc' } }],
query: {
bool: {
filter: [
{
exists: {
field: messageField,
},
},
{
range: {
[timeField]: {
gte: startTimestamp,
lte: endTimestamp,
format: 'strict_date_time',
},
},
},
createCategoryQuery(messageField)(categoryTerms),
...additionalFilters,
],
},
},
};
};

View file

@ -8,11 +8,6 @@
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { ISearchGeneric } from '@kbn/search-types';
import { type DataView } from '@kbn/data-views-plugin/common';
import type { DataTableRecord } from '@kbn/discover-utils';
export interface LogCategoryDocument {
row: Pick<DataTableRecord, 'flattened' | 'raw'>;
}
export interface LogCategoryDetailsParams {
additionalFilters: QueryDslQueryContainer[];
@ -27,5 +22,3 @@ export interface LogCategoryDetailsParams {
export interface CategoryDetailsServiceDependencies {
search: ISearchGeneric;
}
export type LogCategoryDocumentsParams = LogCategoryDetailsParams & { categoryTerms: string };

View file

@ -34,12 +34,9 @@
"@kbn/es-query",
"@kbn/router-utils",
"@kbn/share-plugin",
"@kbn/field-formats-plugin",
"@kbn/data-service",
"@kbn/discover-utils",
"@kbn/discover-plugin",
"@kbn/unified-data-table",
"@kbn/discover-contextual-components",
"@kbn/core-lifecycle-browser",
"@kbn/embeddable-plugin",
"@kbn/data-plugin",
"@kbn/saved-search-component",
]
}

View file

@ -37,7 +37,8 @@
"lens",
"maps",
"uiActions",
"logsDataAccess"
"logsDataAccess",
"savedSearch",
],
"optionalPlugins": [
"actions",

View file

@ -6,9 +6,9 @@
*/
import React, { useMemo } from 'react';
import moment from 'moment';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { LogStream } from '@kbn/logs-shared-plugin/public';
import { LazySavedSearchComponent } from '@kbn/saved-search-component';
import useAsync from 'react-use/lib/useAsync';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
@ -35,6 +35,19 @@ export function ServiceLogs() {
}
export function ClassicServiceLogsStream() {
const {
services: {
logsDataAccess: {
services: { logSourcesService },
},
embeddable,
dataViews,
data: {
search: { searchSource },
},
},
} = useKibana();
const { serviceName } = useApmServiceContext();
const {
@ -62,17 +75,31 @@ export function ClassicServiceLogsStream() {
[environment, kuery, serviceName, start, end]
);
return (
<LogStream
logView={{ type: 'log-view-reference', logViewId: 'default' }}
columns={[{ type: 'timestamp' }, { type: 'message' }]}
height={'60vh'}
startTimestamp={moment(start).valueOf()}
endTimestamp={moment(end).valueOf()}
query={getInfrastructureKQLFilter({ data, serviceName, environment })}
showFlyoutAction
/>
const logSources = useAsync(logSourcesService.getFlattenedLogSources);
const timeRange = useMemo(() => ({ from: start, to: end }), [start, end]);
const query = useMemo(
() => ({
language: 'kuery',
query: getInfrastructureKQLFilter({ data, serviceName, environment }),
}),
[data, serviceName, environment]
);
return logSources.value ? (
<LazySavedSearchComponent
dependencies={{ embeddable, searchSource, dataViews }}
index={logSources.value}
timeRange={timeRange}
query={query}
height={'60vh'}
displayOptions={{
enableDocumentViewer: true,
enableFilters: false,
}}
/>
) : null;
}
export function ServiceLogsOverview() {

View file

@ -70,6 +70,8 @@ import { map } from 'rxjs';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import { registerEmbeddables } from './embeddable/register_embeddables';
@ -143,6 +145,8 @@ export interface ApmPluginStartDeps {
metricsDataAccess: MetricsDataPluginStart;
uiSettings: IUiSettingsClient;
logsShared: LogsSharedClientStartExports;
logsDataAccess: LogsDataAccessPluginStart;
savedSearch: SavedSearchPublicPluginStart;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {

View file

@ -127,6 +127,8 @@
"@kbn/router-utils",
"@kbn/react-hooks",
"@kbn/alerting-comparators",
"@kbn/saved-search-component",
"@kbn/saved-search-plugin",
],
"exclude": ["target/**/*"]
}

View file

@ -12,23 +12,32 @@ import { RegisterServicesParams } from '../register_services';
export function createLogSourcesService(params: RegisterServicesParams): LogSourcesService {
const { uiSettings } = params.deps;
return {
async getLogSources() {
const logSources = uiSettings.get<string[]>(OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID);
return logSources.map((logSource) => ({
indexPattern: logSource,
}));
},
async getFlattenedLogSources() {
const logSources = await this.getLogSources();
return flattenLogSources(logSources);
},
async setLogSources(sources: LogSource[]) {
await uiSettings.set(
OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID,
sources.map((source) => source.indexPattern)
);
return;
},
const getLogSources = async () => {
const logSources = uiSettings.get<string[]>(OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID);
return logSources.map((logSource) => ({
indexPattern: logSource,
}));
};
const getFlattenedLogSources = async () => {
const logSources = await getLogSources();
return flattenLogSources(logSources);
};
const setLogSources = async (sources: LogSource[]) => {
await uiSettings.set(
OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID,
sources.map((source) => source.indexPattern)
);
return;
};
const logSourcesService: LogSourcesService = {
getLogSources,
getFlattenedLogSources,
setLogSources,
};
return logSourcesService;
}

View file

@ -18,11 +18,13 @@
"observabilityShared",
"share",
"usageCollection",
"embeddable",
],
"optionalPlugins": [
"observabilityAIAssistant",
],
"requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedDocViewer"],
"requiredBundles": ["kibanaUtils", "kibanaReact"],
"extraPublicDirs": ["common"]
}
}

View file

@ -61,7 +61,6 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
logsDataAccess,
observabilityAIAssistant,
share,
fieldFormats,
} = plugins;
const logViews = this.logViews.start({
@ -72,14 +71,14 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
});
const LogsOverview = createLogsOverview({
core,
charts,
logsDataAccess,
search: data.search.search,
searchSource: data.search.searchSource,
uiSettings: settings,
share,
dataViews,
fieldFormats,
embeddable: plugins.embeddable,
});
if (!observabilityAIAssistant) {

View file

@ -15,6 +15,8 @@ import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { LogsSharedLocators } from '../common/locators';
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
import type { SelfContainedLogsOverview } from './components/logs_overview';
@ -46,6 +48,8 @@ export interface LogsSharedClientStartDeps {
share: SharePluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
embeddable: EmbeddableStart;
savedSearch: SavedSearchPublicPluginStart;
}
export type LogsSharedClientCoreSetup = CoreSetup<

View file

@ -49,5 +49,7 @@
"@kbn/charts-plugin",
"@kbn/core-ui-settings-common",
"@kbn/field-formats-plugin",
"@kbn/embeddable-plugin",
"@kbn/saved-search-plugin",
]
}

View file

@ -6876,6 +6876,10 @@
version "0.0.0"
uid ""
"@kbn/saved-search-component@link:packages/kbn-saved-search-component":
version "0.0.0"
uid ""
"@kbn/saved-search-plugin@link:src/plugins/saved_search":
version "0.0.0"
uid ""