Skip to content

Commit

Permalink
Add new saved search component
Browse files Browse the repository at this point in the history
  • Loading branch information
Kerry350 committed Nov 12, 2024
1 parent 961796d commit 45ae4a7
Show file tree
Hide file tree
Showing 29 changed files with 483 additions and 176 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,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-screenshotting-server @elastic/appex-sharedux
packages/kbn-search-api-keys-components @elastic/search-kibana
packages/kbn-search-api-keys-server @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 @@ -777,6 +777,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
}
160 changes: 160 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,160 @@
/*
* 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 { SavedSearchComponentProps } from '../types';

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 { dependencies, timeRange, query, filters, index, timestampField, height } = props;

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() {
const { dataViews, searchSource: searchSourceService } = dependencies;
// 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,
} as SearchEmbeddableSerializedState,
references,
});
}
}

createInitialSerializedState();

return () => {
abortController.abort();
};
}, [dependencies, filters, index, query, timeRange, timestampField]);

return initialSerializedState ? (
<div style={{ height: height ?? '100%' }}>
<SavedSearchComponentTable {...props} initialSerializedState={initialSerializedState} />
</div>
) : null;
};

const SavedSearchComponentTable: React.FC<
SavedSearchComponentProps & { initialSerializedState: any }
> = (props) => {
const { dependencies, 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(indexPattern: string) {
const { dataViews } = dependencies;
// Ad-hoc data view
const dataView = await dataViews.create({
title: index,
timeFieldName: timestampField ?? TIMESTAMP_FIELD,
});
if (!abortController.signal.aborted) {
embeddableApi?.current?.setDataViews([dataView]);
}
}

updateDataView(index);

return () => {
abortController.abort();
};
},
[dependencies, 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;
}}
/>
);
};
30 changes: 30 additions & 0 deletions packages/kbn-saved-search-component/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { DataViewsContract, ISearchStartSearchSource } from '@kbn/data-plugin/public';

export interface SavedSearchComponentDependencies {
embeddable: EmbeddableStart;
savedSearch: SavedSearchPublicPluginStart;
searchSource: ISearchStartSearchSource;
dataViews: DataViewsContract;
}

export interface SavedSearchComponentProps {
dependencies: SavedSearchComponentDependencies;
index: string;
timeRange?: TimeRange;
query?: Query;
filters?: Filter[];
timestampField?: string;
height?: string | number;
}
28 changes: 28 additions & 0 deletions packages/kbn-saved-search-component/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/embeddable-plugin",
"@kbn/shared-ux-utility",
"@kbn/discover-utils",
"@kbn/saved-search-plugin",
"@kbn/es-query",
"@kbn/data-plugin",
"@kbn/discover-plugin",
"@kbn/presentation-containers",
]
}
6 changes: 5 additions & 1 deletion packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -144,6 +144,25 @@ export const initializeSearchEmbeddableApi = async (
pick(stateManager, EDITABLE_SAVED_SEARCH_KEYS)
);

/** APIs for updating search source properties */
const setDataViews = async (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(
Expand All @@ -163,10 +182,13 @@ export const initializeSearchEmbeddableApi = async (
syncSavedSearch.unsubscribe();
},
api: {
setDataViews,
dataViews,
savedSearch$,
filters$,
setFilters,
query$,
setQuery,
},
stateManager,
comparators: {
Expand Down
Loading

0 comments on commit 45ae4a7

Please sign in to comment.