Skip to content

Commit

Permalink
[Logs+] Add Filter Control Customization Point (#162013)
Browse files Browse the repository at this point in the history
closes #158561

## 📝  Summary

This PR adds a new customization point to allow for prepending custom
filter controls to the search bar.
At the moment we are only showing a default namespace filter, once this
is ready we will then check how to provide curated filters per
integration.

## ✅  Testing

1. Make sure to have some documents to different data sets with
different namespace, you can use [this
document](https://www.elastic.co/guide/en/elasticsearch/reference/current/set-up-a-data-stream.html)
as an example
2. Navigate to Discover with the log-explorer profile, `/p/log-explorer`
3. Validate that the new filter control is there
4. Filter using this new control and make sure documents are filtered
out

## 🎥 Demo


https://github.com/elastic/kibana/assets/11225826/6828f62f-dd09-42bd-930c-dd7eaf94958b

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
mohamedhamed-ahmed and kibanamachine authored Aug 2, 2023
1 parent 10c09d1 commit 0c9afa1
Show file tree
Hide file tree
Showing 34 changed files with 819 additions and 52 deletions.
2 changes: 1 addition & 1 deletion examples/discover_customization_examples/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"id": "discoverCustomizationExamples",
"server": false,
"browser": true,
"requiredPlugins": ["developerExamples", "discover"]
"requiredPlugins": ["controls", "developerExamples", "discover", "embeddable", "kibanaUtils"]
}
}
164 changes: 164 additions & 0 deletions examples/discover_customization_examples/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import { noop } from 'lodash';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import useObservable from 'react-use/lib/useObservable';
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { css } from '@emotion/react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { ControlsPanels } from '@kbn/controls-plugin/common';
import image from './discover_customization_examples.png';

export interface DiscoverCustomizationExamplesSetupPlugins {
Expand Down Expand Up @@ -228,6 +232,166 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
},
});

customizations.set({
id: 'search_bar',
CustomDataViewPicker: () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const togglePopover = () => setIsPopoverOpen((open) => !open);
const closePopover = () => setIsPopoverOpen(false);
const [savedSearches, setSavedSearches] = useState<
Array<SimpleSavedObject<{ title: string }>>
>([]);

useEffect(() => {
core.savedObjects.client
.find<{ title: string }>({ type: 'search' })
.then((response) => {
setSavedSearches(response.savedObjects);
});
}, []);

const currentSavedSearch = useObservable(
stateContainer.savedSearchState.getCurrent$(),
stateContainer.savedSearchState.getState()
);

return (
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButton
iconType="arrowDown"
iconSide="right"
fullWidth
onClick={togglePopover}
data-test-subj="logsViewSelectorButton"
>
{currentSavedSearch.title ?? 'None selected'}
</EuiButton>
}
anchorClassName="eui-fullWidth"
isOpen={isPopoverOpen}
panelPaddingSize="none"
closePopover={closePopover}
>
<EuiContextMenu
size="s"
initialPanelId={0}
panels={[
{
id: 0,
title: 'Saved logs views',
items: savedSearches.map((savedSearch) => ({
name: savedSearch.get('title'),
onClick: () => stateContainer.actions.onOpenSavedSearch(savedSearch.id),
icon: savedSearch.id === currentSavedSearch.id ? 'check' : 'empty',
'data-test-subj': `logsViewSelectorOption-${savedSearch.attributes.title.replace(
/[^a-zA-Z0-9]/g,
''
)}`,
})),
},
]}
/>
</EuiPopover>
</EuiFlexItem>
);
},
PrependFilterBar: () => {
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
const stateStorage = stateContainer.stateStorage;
const dataView = useObservable(
stateContainer.internalState.state$,
stateContainer.internalState.getState()
).dataView;

useEffect(() => {
if (!controlGroupAPI) {
return;
}

const stateSubscription = stateStorage
.change$<ControlsPanels>('controlPanels')
.subscribe((panels) =>
controlGroupAPI.updateInput({ panels: panels ?? undefined })
);

const inputSubscription = controlGroupAPI.getInput$().subscribe((input) => {
if (input && input.panels) stateStorage.set('controlPanels', input.panels);
});

const filterSubscription = controlGroupAPI.onFiltersPublished$.subscribe(
(newFilters) => {
stateContainer.internalState.transitions.setCustomFilters(newFilters);
stateContainer.actions.fetchData();
}
);

return () => {
stateSubscription.unsubscribe();
inputSubscription.unsubscribe();
filterSubscription.unsubscribe();
};
}, [controlGroupAPI, stateStorage]);

const fieldToFilterOn = dataView?.fields.filter((field) =>
field.esTypes?.includes('keyword')
)[0];

if (!fieldToFilterOn) {
return null;
}

return (
<EuiFlexItem
data-test-subj="customPrependedFilter"
grow={false}
css={css`
.controlGroup {
min-height: unset;
}
.euiFormLabel {
padding-top: 0;
padding-bottom: 0;
line-height: 32px !important;
}
.euiFormControlLayout {
height: 32px;
}
`}
>
<ControlGroupRenderer
ref={setControlGroupAPI}
getCreationOptions={async (initialInput, builder) => {
const panels = stateStorage.get<ControlsPanels>('controlPanels');

if (!panels) {
await builder.addOptionsListControl(initialInput, {
dataViewId: dataView?.id!,
title: fieldToFilterOn.name.split('.')[0],
fieldName: fieldToFilterOn.name,
grow: false,
width: 'small',
});
}

return {
initialInput: {
...initialInput,
panels: panels ?? initialInput.panels,
viewMode: ViewMode.VIEW,
filters: stateContainer.appState.get().filters ?? [],
},
};
}}
/>
</EuiFlexItem>
);
},
});

return () => {
// eslint-disable-next-line no-console
console.log('Cleaning up Logs explorer customizations');
Expand Down
2 changes: 2 additions & 0 deletions examples/discover_customization_examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"@kbn/core",
"@kbn/discover-plugin",
"@kbn/developer-examples-plugin",
"@kbn/controls-plugin",
"@kbn/embeddable-plugin",
],
"exclude": ["target/**/*"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,10 @@ export const optionsListReducers = {
) => {
state.output.dataViewId = action.payload;
},
setExplicitInputDataViewId: (
state: WritableDraft<OptionsListReduxState>,
action: PayloadAction<string>
) => {
state.explicitInput.dataViewId = action.payload;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export const useDiscoverHistogram = ({
* Request params
*/
const { query, filters } = useQuerySubscriber({ data: services.data });
const customFilters = useInternalStateSelector((state) => state.customFilters);
const timefilter = services.data.query.timefilter.timefilter;
const timeRange = timefilter.getAbsoluteTime();
const relativeTimeRange = useObservable(
Expand Down Expand Up @@ -305,7 +306,7 @@ export const useDiscoverHistogram = ({
services: { ...services, uiActions: getUiActions() },
dataView: isPlainRecord ? textBasedDataView : dataView,
query: isPlainRecord ? textBasedQuery : query,
filters,
filters: [...(filters ?? []), ...customFilters],
timeRange,
relativeTimeRange,
columns,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ export const DiscoverTopNav = ({
textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined
}
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
prependFilterBar={
searchBarCustomization?.PrependFilterBar ? (
<searchBarCustomization.PrependFilterBar />
) : undefined
}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { validateTimeRange } from '../utils/validate_time_range';
import { fetchAll } from '../utils/fetch_all';
import { sendResetMsg } from '../hooks/use_saved_search_messages';
import { getFetch$ } from '../utils/get_fetch_observable';
import { InternalState } from './discover_internal_state_container';

export interface SavedSearchData {
main$: DataMain$;
Expand Down Expand Up @@ -132,12 +133,14 @@ export function getDataStateContainer({
services,
searchSessionManager,
getAppState,
getInternalState,
getSavedSearch,
setDataView,
}: {
services: DiscoverServices;
searchSessionManager: DiscoverSearchSessionManager;
getAppState: () => DiscoverAppState;
getInternalState: () => InternalState;
getSavedSearch: () => SavedSearch;
setDataView: (dataView: DataView) => void;
}): DiscoverDataStateContainer {
Expand Down Expand Up @@ -213,6 +216,7 @@ export function getDataStateContainer({
searchSessionId,
services,
getAppState,
getInternalState,
savedSearch: getSavedSearch(),
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
ReduxLikeStateContainer,
} from '@kbn/kibana-utils-plugin/common';
import { DataView, DataViewListItem } from '@kbn/data-views-plugin/common';
import { Filter } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';

export interface InternalState {
dataView: DataView | undefined;
savedDataViews: DataViewListItem[];
adHocDataViews: DataView[];
expandedDoc: DataTableRecord | undefined;
customFilters: Filter[];
}

interface InternalStateTransitions {
Expand All @@ -35,6 +37,7 @@ interface InternalStateTransitions {
setExpandedDoc: (
state: InternalState
) => (dataView: DataTableRecord | undefined) => InternalState;
setCustomFilters: (state: InternalState) => (customFilters: Filter[]) => InternalState;
}

export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
Expand All @@ -52,6 +55,7 @@ export function getInternalStateContainer() {
adHocDataViews: [],
savedDataViews: [],
expandedDoc: undefined,
customFilters: [],
},
{
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
Expand Down Expand Up @@ -97,6 +101,10 @@ export function getInternalStateContainer() {
...prevState,
expandedDoc,
}),
setCustomFilters: (prevState: InternalState) => (customFilters: Filter[]) => ({
...prevState,
customFilters,
}),
},
{},
{ freeze: (state) => state }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { History } from 'history';
import {
createKbnUrlStateStorage,
IKbnUrlStateStorage,
StateContainer,
withNotifyOnErrors,
} from '@kbn/kibana-utils-plugin/public';
Expand Down Expand Up @@ -97,6 +98,10 @@ export interface DiscoverStateContainer {
* State of saved search, the saved object of Discover
*/
savedSearchState: DiscoverSavedSearchContainer;
/**
* State of url, allows updating and subscribing to url changes
*/
stateStorage: IKbnUrlStateStorage;
/**
* Service for handling search sessions
*/
Expand Down Expand Up @@ -252,6 +257,7 @@ export function getDiscoverStateContainer({
services,
searchSessionManager,
getAppState: appStateContainer.getState,
getInternalState: internalStateContainer.getState,
getSavedSearch: savedSearchContainer.getState,
setDataView,
});
Expand Down Expand Up @@ -451,6 +457,7 @@ export function getDiscoverStateContainer({
internalState: internalStateContainer,
dataState: dataStateContainer,
savedSearchState: savedSearchContainer,
stateStorage,
searchSessionManager,
actions: {
initializeAndSync,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ describe('test fetchAll', () => {
abortController: new AbortController(),
inspectorAdapters: { requests: new RequestAdapter() },
getAppState: () => ({}),
getInternalState: () => ({
dataView: undefined,
savedDataViews: [],
adHocDataViews: [],
expandedDoc: undefined,
customFilters: [],
}),
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
useNewFieldsApi: true,
Expand Down Expand Up @@ -258,6 +265,13 @@ describe('test fetchAll', () => {
savedSearch: savedSearchMock,
services: discoverServiceMock,
getAppState: () => ({ query }),
getInternalState: () => ({
dataView: undefined,
savedDataViews: [],
adHocDataViews: [],
expandedDoc: undefined,
customFilters: [],
}),
};
fetchAll(subjects, false, deps);
await waitForNextTick();
Expand Down
Loading

0 comments on commit 0c9afa1

Please sign in to comment.