Skip to content

Commit

Permalink
[Discover / Logs] Add new "Saved Search component" (elastic#199787)
Browse files Browse the repository at this point in the history
## Summary

Implements
elastic/logs-dev#111 (comment).

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
👌](elastic/logs-dev#111 (comment)).

`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 <[email protected]>
  • Loading branch information
2 people authored and CAWilson94 committed Dec 9, 2024
1 parent 3e0f408 commit f67f8fe
Show file tree
Hide file tree
Showing 39 changed files with 636 additions and 476 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions packages/kbn-saved-search-component/README.md
Original file line number Diff line number Diff line change
@@ -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}
/>
```
18 changes: 18 additions & 0 deletions packages/kbn-saved-search-component/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}))
);
14 changes: 14 additions & 0 deletions packages/kbn-saved-search-component/jest.config.js
Original file line number Diff line number Diff line change
@@ -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'],
};
5 changes: 5 additions & 0 deletions packages/kbn-saved-search-component/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/saved-search-component",
"owner": "@elastic/obs-ux-logs-team"
}
7 changes: 7 additions & 0 deletions packages/kbn-saved-search-component/package.json
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions packages/kbn-saved-search-component/src/components/error.tsx
Original file line number Diff line number Diff line change
@@ -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.',
});
214 changes: 214 additions & 0 deletions packages/kbn-saved-search-component/src/components/saved_search.tsx
Original file line number Diff line number Diff line change
@@ -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
/>
);
};
31 changes: 31 additions & 0 deletions packages/kbn-saved-search-component/src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit f67f8fe

Please sign in to comment.