Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,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,
Kerry350 marked this conversation as resolved.
Show resolved Hide resolved
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;
}}
Kerry350 marked this conversation as resolved.
Show resolved Hide resolved
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