mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Logs+] Add Filter Control Customization Point (#162013)
closes https://github.com/elastic/kibana/issues/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
6828f62f
-dd09-42bd-930c-dd7eaf94958b
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
10c09d140a
commit
0c9afa1442
34 changed files with 819 additions and 52 deletions
|
@ -6,6 +6,6 @@
|
||||||
"id": "discoverCustomizationExamples",
|
"id": "discoverCustomizationExamples",
|
||||||
"server": false,
|
"server": false,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"requiredPlugins": ["developerExamples", "discover"]
|
"requiredPlugins": ["controls", "developerExamples", "discover", "embeddable", "kibanaUtils"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ import { noop } from 'lodash';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
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';
|
import image from './discover_customization_examples.png';
|
||||||
|
|
||||||
export interface DiscoverCustomizationExamplesSetupPlugins {
|
export interface DiscoverCustomizationExamplesSetupPlugins {
|
||||||
|
@ -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 () => {
|
return () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('Cleaning up Logs explorer customizations');
|
console.log('Cleaning up Logs explorer customizations');
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
"@kbn/core",
|
"@kbn/core",
|
||||||
"@kbn/discover-plugin",
|
"@kbn/discover-plugin",
|
||||||
"@kbn/developer-examples-plugin",
|
"@kbn/developer-examples-plugin",
|
||||||
|
"@kbn/controls-plugin",
|
||||||
|
"@kbn/embeddable-plugin",
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": ["target/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,4 +144,10 @@ export const optionsListReducers = {
|
||||||
) => {
|
) => {
|
||||||
state.output.dataViewId = action.payload;
|
state.output.dataViewId = action.payload;
|
||||||
},
|
},
|
||||||
|
setExplicitInputDataViewId: (
|
||||||
|
state: WritableDraft<OptionsListReduxState>,
|
||||||
|
action: PayloadAction<string>
|
||||||
|
) => {
|
||||||
|
state.explicitInput.dataViewId = action.payload;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -215,6 +215,7 @@ export const useDiscoverHistogram = ({
|
||||||
* Request params
|
* Request params
|
||||||
*/
|
*/
|
||||||
const { query, filters } = useQuerySubscriber({ data: services.data });
|
const { query, filters } = useQuerySubscriber({ data: services.data });
|
||||||
|
const customFilters = useInternalStateSelector((state) => state.customFilters);
|
||||||
const timefilter = services.data.query.timefilter.timefilter;
|
const timefilter = services.data.query.timefilter.timefilter;
|
||||||
const timeRange = timefilter.getAbsoluteTime();
|
const timeRange = timefilter.getAbsoluteTime();
|
||||||
const relativeTimeRange = useObservable(
|
const relativeTimeRange = useObservable(
|
||||||
|
@ -305,7 +306,7 @@ export const useDiscoverHistogram = ({
|
||||||
services: { ...services, uiActions: getUiActions() },
|
services: { ...services, uiActions: getUiActions() },
|
||||||
dataView: isPlainRecord ? textBasedDataView : dataView,
|
dataView: isPlainRecord ? textBasedDataView : dataView,
|
||||||
query: isPlainRecord ? textBasedQuery : query,
|
query: isPlainRecord ? textBasedQuery : query,
|
||||||
filters,
|
filters: [...(filters ?? []), ...customFilters],
|
||||||
timeRange,
|
timeRange,
|
||||||
relativeTimeRange,
|
relativeTimeRange,
|
||||||
columns,
|
columns,
|
||||||
|
|
|
@ -234,6 +234,11 @@ export const DiscoverTopNav = ({
|
||||||
textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined
|
textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined
|
||||||
}
|
}
|
||||||
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
|
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
|
||||||
|
prependFilterBar={
|
||||||
|
searchBarCustomization?.PrependFilterBar ? (
|
||||||
|
<searchBarCustomization.PrependFilterBar />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { validateTimeRange } from '../utils/validate_time_range';
|
||||||
import { fetchAll } from '../utils/fetch_all';
|
import { fetchAll } from '../utils/fetch_all';
|
||||||
import { sendResetMsg } from '../hooks/use_saved_search_messages';
|
import { sendResetMsg } from '../hooks/use_saved_search_messages';
|
||||||
import { getFetch$ } from '../utils/get_fetch_observable';
|
import { getFetch$ } from '../utils/get_fetch_observable';
|
||||||
|
import { InternalState } from './discover_internal_state_container';
|
||||||
|
|
||||||
export interface SavedSearchData {
|
export interface SavedSearchData {
|
||||||
main$: DataMain$;
|
main$: DataMain$;
|
||||||
|
@ -132,12 +133,14 @@ export function getDataStateContainer({
|
||||||
services,
|
services,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
getAppState,
|
getAppState,
|
||||||
|
getInternalState,
|
||||||
getSavedSearch,
|
getSavedSearch,
|
||||||
setDataView,
|
setDataView,
|
||||||
}: {
|
}: {
|
||||||
services: DiscoverServices;
|
services: DiscoverServices;
|
||||||
searchSessionManager: DiscoverSearchSessionManager;
|
searchSessionManager: DiscoverSearchSessionManager;
|
||||||
getAppState: () => DiscoverAppState;
|
getAppState: () => DiscoverAppState;
|
||||||
|
getInternalState: () => InternalState;
|
||||||
getSavedSearch: () => SavedSearch;
|
getSavedSearch: () => SavedSearch;
|
||||||
setDataView: (dataView: DataView) => void;
|
setDataView: (dataView: DataView) => void;
|
||||||
}): DiscoverDataStateContainer {
|
}): DiscoverDataStateContainer {
|
||||||
|
@ -213,6 +216,7 @@ export function getDataStateContainer({
|
||||||
searchSessionId,
|
searchSessionId,
|
||||||
services,
|
services,
|
||||||
getAppState,
|
getAppState,
|
||||||
|
getInternalState,
|
||||||
savedSearch: getSavedSearch(),
|
savedSearch: getSavedSearch(),
|
||||||
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
|
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
ReduxLikeStateContainer,
|
ReduxLikeStateContainer,
|
||||||
} from '@kbn/kibana-utils-plugin/common';
|
} from '@kbn/kibana-utils-plugin/common';
|
||||||
import { DataView, DataViewListItem } from '@kbn/data-views-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';
|
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||||
|
|
||||||
export interface InternalState {
|
export interface InternalState {
|
||||||
|
@ -19,6 +20,7 @@ export interface InternalState {
|
||||||
savedDataViews: DataViewListItem[];
|
savedDataViews: DataViewListItem[];
|
||||||
adHocDataViews: DataView[];
|
adHocDataViews: DataView[];
|
||||||
expandedDoc: DataTableRecord | undefined;
|
expandedDoc: DataTableRecord | undefined;
|
||||||
|
customFilters: Filter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InternalStateTransitions {
|
interface InternalStateTransitions {
|
||||||
|
@ -35,6 +37,7 @@ interface InternalStateTransitions {
|
||||||
setExpandedDoc: (
|
setExpandedDoc: (
|
||||||
state: InternalState
|
state: InternalState
|
||||||
) => (dataView: DataTableRecord | undefined) => InternalState;
|
) => (dataView: DataTableRecord | undefined) => InternalState;
|
||||||
|
setCustomFilters: (state: InternalState) => (customFilters: Filter[]) => InternalState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
|
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
|
||||||
|
@ -52,6 +55,7 @@ export function getInternalStateContainer() {
|
||||||
adHocDataViews: [],
|
adHocDataViews: [],
|
||||||
savedDataViews: [],
|
savedDataViews: [],
|
||||||
expandedDoc: undefined,
|
expandedDoc: undefined,
|
||||||
|
customFilters: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
|
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
|
||||||
|
@ -97,6 +101,10 @@ export function getInternalStateContainer() {
|
||||||
...prevState,
|
...prevState,
|
||||||
expandedDoc,
|
expandedDoc,
|
||||||
}),
|
}),
|
||||||
|
setCustomFilters: (prevState: InternalState) => (customFilters: Filter[]) => ({
|
||||||
|
...prevState,
|
||||||
|
customFilters,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{ freeze: (state) => state }
|
{ freeze: (state) => state }
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import {
|
import {
|
||||||
createKbnUrlStateStorage,
|
createKbnUrlStateStorage,
|
||||||
|
IKbnUrlStateStorage,
|
||||||
StateContainer,
|
StateContainer,
|
||||||
withNotifyOnErrors,
|
withNotifyOnErrors,
|
||||||
} from '@kbn/kibana-utils-plugin/public';
|
} from '@kbn/kibana-utils-plugin/public';
|
||||||
|
@ -97,6 +98,10 @@ export interface DiscoverStateContainer {
|
||||||
* State of saved search, the saved object of Discover
|
* State of saved search, the saved object of Discover
|
||||||
*/
|
*/
|
||||||
savedSearchState: DiscoverSavedSearchContainer;
|
savedSearchState: DiscoverSavedSearchContainer;
|
||||||
|
/**
|
||||||
|
* State of url, allows updating and subscribing to url changes
|
||||||
|
*/
|
||||||
|
stateStorage: IKbnUrlStateStorage;
|
||||||
/**
|
/**
|
||||||
* Service for handling search sessions
|
* Service for handling search sessions
|
||||||
*/
|
*/
|
||||||
|
@ -252,6 +257,7 @@ export function getDiscoverStateContainer({
|
||||||
services,
|
services,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
getAppState: appStateContainer.getState,
|
getAppState: appStateContainer.getState,
|
||||||
|
getInternalState: internalStateContainer.getState,
|
||||||
getSavedSearch: savedSearchContainer.getState,
|
getSavedSearch: savedSearchContainer.getState,
|
||||||
setDataView,
|
setDataView,
|
||||||
});
|
});
|
||||||
|
@ -451,6 +457,7 @@ export function getDiscoverStateContainer({
|
||||||
internalState: internalStateContainer,
|
internalState: internalStateContainer,
|
||||||
dataState: dataStateContainer,
|
dataState: dataStateContainer,
|
||||||
savedSearchState: savedSearchContainer,
|
savedSearchState: savedSearchContainer,
|
||||||
|
stateStorage,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
actions: {
|
actions: {
|
||||||
initializeAndSync,
|
initializeAndSync,
|
||||||
|
|
|
@ -68,6 +68,13 @@ describe('test fetchAll', () => {
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
inspectorAdapters: { requests: new RequestAdapter() },
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
getAppState: () => ({}),
|
getAppState: () => ({}),
|
||||||
|
getInternalState: () => ({
|
||||||
|
dataView: undefined,
|
||||||
|
savedDataViews: [],
|
||||||
|
adHocDataViews: [],
|
||||||
|
expandedDoc: undefined,
|
||||||
|
customFilters: [],
|
||||||
|
}),
|
||||||
searchSessionId: '123',
|
searchSessionId: '123',
|
||||||
initialFetchStatus: FetchStatus.UNINITIALIZED,
|
initialFetchStatus: FetchStatus.UNINITIALIZED,
|
||||||
useNewFieldsApi: true,
|
useNewFieldsApi: true,
|
||||||
|
@ -258,6 +265,13 @@ describe('test fetchAll', () => {
|
||||||
savedSearch: savedSearchMock,
|
savedSearch: savedSearchMock,
|
||||||
services: discoverServiceMock,
|
services: discoverServiceMock,
|
||||||
getAppState: () => ({ query }),
|
getAppState: () => ({ query }),
|
||||||
|
getInternalState: () => ({
|
||||||
|
dataView: undefined,
|
||||||
|
savedDataViews: [],
|
||||||
|
adHocDataViews: [],
|
||||||
|
expandedDoc: undefined,
|
||||||
|
customFilters: [],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
fetchAll(subjects, false, deps);
|
fetchAll(subjects, false, deps);
|
||||||
await waitForNextTick();
|
await waitForNextTick();
|
||||||
|
|
|
@ -26,10 +26,12 @@ import { FetchStatus } from '../../types';
|
||||||
import { DataMsg, RecordRawType, SavedSearchData } from '../services/discover_data_state_container';
|
import { DataMsg, RecordRawType, SavedSearchData } from '../services/discover_data_state_container';
|
||||||
import { DiscoverServices } from '../../../build_services';
|
import { DiscoverServices } from '../../../build_services';
|
||||||
import { fetchSql } from './fetch_sql';
|
import { fetchSql } from './fetch_sql';
|
||||||
|
import { InternalState } from '../services/discover_internal_state_container';
|
||||||
|
|
||||||
export interface FetchDeps {
|
export interface FetchDeps {
|
||||||
abortController: AbortController;
|
abortController: AbortController;
|
||||||
getAppState: () => DiscoverAppState;
|
getAppState: () => DiscoverAppState;
|
||||||
|
getInternalState: () => InternalState;
|
||||||
initialFetchStatus: FetchStatus;
|
initialFetchStatus: FetchStatus;
|
||||||
inspectorAdapters: Adapters;
|
inspectorAdapters: Adapters;
|
||||||
savedSearch: SavedSearch;
|
savedSearch: SavedSearch;
|
||||||
|
@ -50,7 +52,14 @@ export function fetchAll(
|
||||||
reset = false,
|
reset = false,
|
||||||
fetchDeps: FetchDeps
|
fetchDeps: FetchDeps
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { initialFetchStatus, getAppState, services, inspectorAdapters, savedSearch } = fetchDeps;
|
const {
|
||||||
|
initialFetchStatus,
|
||||||
|
getAppState,
|
||||||
|
getInternalState,
|
||||||
|
services,
|
||||||
|
inspectorAdapters,
|
||||||
|
savedSearch,
|
||||||
|
} = fetchDeps;
|
||||||
const { data } = services;
|
const { data } = services;
|
||||||
const searchSource = savedSearch.searchSource.createChild();
|
const searchSource = savedSearch.searchSource.createChild();
|
||||||
|
|
||||||
|
@ -70,6 +79,7 @@ export function fetchAll(
|
||||||
dataView,
|
dataView,
|
||||||
services,
|
services,
|
||||||
sort: getAppState().sort as SortOrder[],
|
sort: getAppState().sort as SortOrder[],
|
||||||
|
customFilters: getInternalState().customFilters,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||||
|
import { Filter } from '@kbn/es-query';
|
||||||
|
|
||||||
const getUiSettingsMock = (value: boolean) => {
|
const getUiSettingsMock = (value: boolean) => {
|
||||||
return {
|
return {
|
||||||
|
@ -27,6 +28,7 @@ describe('updateVolatileSearchSource', () => {
|
||||||
dataView: dataViewMock,
|
dataView: dataViewMock,
|
||||||
services: discoverServiceMock,
|
services: discoverServiceMock,
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
|
customFilters: [],
|
||||||
});
|
});
|
||||||
expect(searchSource.getField('fields')).toBe(undefined);
|
expect(searchSource.getField('fields')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
@ -38,6 +40,7 @@ describe('updateVolatileSearchSource', () => {
|
||||||
dataView: dataViewMock,
|
dataView: dataViewMock,
|
||||||
services: discoverServiceMock,
|
services: discoverServiceMock,
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
|
customFilters: [],
|
||||||
});
|
});
|
||||||
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
|
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
|
||||||
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
|
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
|
||||||
|
@ -50,6 +53,7 @@ describe('updateVolatileSearchSource', () => {
|
||||||
dataView: dataViewMock,
|
dataView: dataViewMock,
|
||||||
services: discoverServiceMock,
|
services: discoverServiceMock,
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
|
customFilters: [],
|
||||||
});
|
});
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
||||||
{ field: '*', include_unmapped: 'true' },
|
{ field: '*', include_unmapped: 'true' },
|
||||||
|
@ -64,8 +68,25 @@ describe('updateVolatileSearchSource', () => {
|
||||||
dataView: dataViewMock,
|
dataView: dataViewMock,
|
||||||
services: discoverServiceMock,
|
services: discoverServiceMock,
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
|
customFilters: [],
|
||||||
});
|
});
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined);
|
expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined);
|
||||||
expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
|
expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should properly update the search source with the given custom filters', async () => {
|
||||||
|
const searchSource = createSearchSourceMock({});
|
||||||
|
discoverServiceMock.uiSettings = getUiSettingsMock(false);
|
||||||
|
|
||||||
|
const filter = { meta: { index: 'foo', key: 'bar' } } as Filter;
|
||||||
|
|
||||||
|
updateVolatileSearchSource(searchSource, {
|
||||||
|
dataView: dataViewMock,
|
||||||
|
services: discoverServiceMock,
|
||||||
|
sort: [] as SortOrder[],
|
||||||
|
customFilters: [filter],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(searchSource.getField('filter')).toEqual([filter]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import { ISearchSource } from '@kbn/data-plugin/public';
|
import { ISearchSource } from '@kbn/data-plugin/public';
|
||||||
import { DataViewType, DataView } from '@kbn/data-views-plugin/public';
|
import { DataViewType, DataView } from '@kbn/data-views-plugin/public';
|
||||||
|
import { Filter } from '@kbn/es-query';
|
||||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||||
import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
||||||
import { DiscoverServices } from '../../../build_services';
|
import { DiscoverServices } from '../../../build_services';
|
||||||
|
@ -22,10 +23,12 @@ export function updateVolatileSearchSource(
|
||||||
dataView,
|
dataView,
|
||||||
services,
|
services,
|
||||||
sort,
|
sort,
|
||||||
|
customFilters,
|
||||||
}: {
|
}: {
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
services: DiscoverServices;
|
services: DiscoverServices;
|
||||||
sort?: SortOrder[];
|
sort?: SortOrder[];
|
||||||
|
customFilters: Filter[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { uiSettings, data } = services;
|
const { uiSettings, data } = services;
|
||||||
|
@ -37,11 +40,16 @@ export function updateVolatileSearchSource(
|
||||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||||
searchSource.setField('trackTotalHits', true).setField('sort', usedSort);
|
searchSource.setField('trackTotalHits', true).setField('sort', usedSort);
|
||||||
|
|
||||||
|
let filters = [...customFilters];
|
||||||
|
|
||||||
if (dataView.type !== DataViewType.ROLLUP) {
|
if (dataView.type !== DataViewType.ROLLUP) {
|
||||||
// Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range
|
// Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range
|
||||||
searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(dataView));
|
const timeFilter = data.query.timefilter.timefilter.createFilter(dataView);
|
||||||
|
filters = timeFilter ? [...filters, timeFilter] : filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchSource.setField('filter', filters);
|
||||||
|
|
||||||
if (useNewFieldsApi) {
|
if (useNewFieldsApi) {
|
||||||
searchSource.removeField('fieldsFromSource');
|
searchSource.removeField('fieldsFromSource');
|
||||||
const fields: Record<string, string> = { field: '*' };
|
const fields: Record<string, string> = { field: '*' };
|
||||||
|
|
|
@ -13,5 +13,6 @@ import type { ComponentType, ReactElement } from 'react';
|
||||||
export interface SearchBarCustomization {
|
export interface SearchBarCustomization {
|
||||||
id: 'search_bar';
|
id: 'search_bar';
|
||||||
CustomDataViewPicker?: ComponentType;
|
CustomDataViewPicker?: ComponentType;
|
||||||
|
PrependFilterBar?: ComponentType;
|
||||||
CustomSearchBar?: (props: TopNavMenuProps<AggregateQuery>) => ReactElement;
|
CustomSearchBar?: (props: TopNavMenuProps<AggregateQuery>) => ReactElement;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||||
import { buildExistsFilter } from '@kbn/es-query';
|
import { buildExistsFilter } from '@kbn/es-query';
|
||||||
|
import { EuiComboBox } from '@elastic/eui';
|
||||||
import { SearchBar, SearchBarProps } from '../search_bar';
|
import { SearchBar, SearchBarProps } from '../search_bar';
|
||||||
import { setIndexPatterns } from '../services';
|
import { setIndexPatterns } from '../services';
|
||||||
|
|
||||||
|
@ -454,6 +455,23 @@ storiesOf('SearchBar', module)
|
||||||
},
|
},
|
||||||
} as unknown as SearchBarProps)
|
} as unknown as SearchBarProps)
|
||||||
)
|
)
|
||||||
|
.add('with prepended controls', () =>
|
||||||
|
wrapSearchBarInContext({
|
||||||
|
prependFilterBar: (
|
||||||
|
<EuiComboBox
|
||||||
|
placeholder="Select option"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Filter 1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth={false}
|
||||||
|
isClearable={true}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
showQueryInput: true,
|
||||||
|
} as SearchBarProps)
|
||||||
|
)
|
||||||
.add('without switch query language', () =>
|
.add('without switch query language', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
disableQueryLanguageSwitcher: true,
|
disableQueryLanguageSwitcher: true,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import { EuiFlexGroup, useEuiTheme } from '@elastic/eui';
|
import { EuiFlexGroup, useEuiTheme } from '@elastic/eui';
|
||||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||||
import type { Filter } from '@kbn/es-query';
|
import type { Filter } from '@kbn/es-query';
|
||||||
import React, { useRef } from 'react';
|
import React, { ReactNode, useRef } from 'react';
|
||||||
import { DataView } from '@kbn/data-views-plugin/public';
|
import { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import FilterItems, { type FilterItemsProps } from './filter_item/filter_items';
|
import FilterItems, { type FilterItemsProps } from './filter_item/filter_items';
|
||||||
|
|
||||||
|
@ -33,6 +33,10 @@ export interface Props {
|
||||||
* Disable all interactive actions
|
* Disable all interactive actions
|
||||||
*/
|
*/
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Prepends custom filter controls to the search bar
|
||||||
|
*/
|
||||||
|
prepend?: ReactNode;
|
||||||
/** Array of suggestion abstraction that controls the render of the field */
|
/** Array of suggestion abstraction that controls the render of the field */
|
||||||
suggestionsAbstraction?: SuggestionsAbstraction;
|
suggestionsAbstraction?: SuggestionsAbstraction;
|
||||||
}
|
}
|
||||||
|
@ -52,6 +56,7 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) {
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
|
{props.prepend}
|
||||||
<FilterItems
|
<FilterItems
|
||||||
filters={props.filters!}
|
filters={props.filters!}
|
||||||
onFiltersUpdated={props.onFiltersUpdated}
|
onFiltersUpdated={props.onFiltersUpdated}
|
||||||
|
|
|
@ -252,6 +252,7 @@ export function createSearchBar({
|
||||||
isScreenshotMode={isScreenshotMode}
|
isScreenshotMode={isScreenshotMode}
|
||||||
dataTestSubj={props.dataTestSubj}
|
dataTestSubj={props.dataTestSubj}
|
||||||
filtersForSuggestions={props.filtersForSuggestions}
|
filtersForSuggestions={props.filtersForSuggestions}
|
||||||
|
prependFilterBar={props.prependFilterBar}
|
||||||
/>
|
/>
|
||||||
</core.i18n.Context>
|
</core.i18n.Context>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
|
|
|
@ -60,6 +60,7 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
filtersForSuggestions?: Filter[];
|
filtersForSuggestions?: Filter[];
|
||||||
hiddenFilterPanelOptions?: QueryBarMenuProps['hiddenPanelOptions'];
|
hiddenFilterPanelOptions?: QueryBarMenuProps['hiddenPanelOptions'];
|
||||||
|
prependFilterBar?: React.ReactNode;
|
||||||
// Date picker
|
// Date picker
|
||||||
isRefreshPaused?: boolean;
|
isRefreshPaused?: boolean;
|
||||||
refreshInterval?: number;
|
refreshInterval?: number;
|
||||||
|
@ -548,6 +549,7 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
|
||||||
hiddenPanelOptions={this.props.hiddenFilterPanelOptions}
|
hiddenPanelOptions={this.props.hiddenFilterPanelOptions}
|
||||||
isDisabled={this.props.isDisabled}
|
isDisabled={this.props.isDisabled}
|
||||||
data-test-subj="unifiedFilterBar"
|
data-test-subj="unifiedFilterBar"
|
||||||
|
prepend={this.props.prependFilterBar}
|
||||||
suggestionsAbstraction={this.props.suggestionsAbstraction}
|
suggestionsAbstraction={this.props.suggestionsAbstraction}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||||
const kibanaServer = getService('kibanaServer');
|
const kibanaServer = getService('kibanaServer');
|
||||||
const testSubjects = getService('testSubjects');
|
const testSubjects = getService('testSubjects');
|
||||||
const browser = getService('browser');
|
const browser = getService('browser');
|
||||||
|
const dataGrid = getService('dataGrid');
|
||||||
const defaultSettings = { defaultIndex: 'logstash-*' };
|
const defaultSettings = { defaultIndex: 'logstash-*' };
|
||||||
|
|
||||||
describe('Customizations', () => {
|
describe('Customizations', () => {
|
||||||
|
@ -68,5 +69,33 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||||
expect(title).to.eql(expected.title);
|
expect(title).to.eql(expected.title);
|
||||||
expect(description).to.eql(expected.description);
|
expect(description).to.eql(expected.description);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Search bar Prepend Filters exists and should apply filter properly', async () => {
|
||||||
|
// Validate custom filters are present
|
||||||
|
await testSubjects.existOrFail('customPrependedFilter');
|
||||||
|
await testSubjects.click('customPrependedFilter');
|
||||||
|
await testSubjects.existOrFail('optionsList-control-selection-exists');
|
||||||
|
|
||||||
|
// Retrieve option list popover
|
||||||
|
const optionsListControl = await testSubjects.find('optionsList-control-popover');
|
||||||
|
const optionsItems = await optionsListControl.findAllByCssSelector(
|
||||||
|
'[data-test-subj*="optionsList-control-selection-"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retrieve second item in the options along with the count of documents
|
||||||
|
const item = optionsItems[1];
|
||||||
|
const countBadge = await item.findByCssSelector(
|
||||||
|
'[data-test-subj="optionsList-document-count-badge"]'
|
||||||
|
);
|
||||||
|
const documentsCount = parseInt(await countBadge.getVisibleText(), 10);
|
||||||
|
|
||||||
|
// Click the item to apply filter
|
||||||
|
await item.click();
|
||||||
|
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||||
|
|
||||||
|
// Validate that filter is applied
|
||||||
|
const rows = await dataGrid.getDocTableRows();
|
||||||
|
await expect(documentsCount).to.eql(rows.length);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"configPath": ["xpack", "discoverLogExplorer"],
|
"configPath": ["xpack", "discoverLogExplorer"],
|
||||||
"requiredPlugins": ["discover", "fleet", "kibanaReact", "kibanaUtils"],
|
"requiredPlugins": ["data", "dataViews", "discover", "fleet", "kibanaReact", "kibanaUtils", "controls", "embeddable"],
|
||||||
"optionalPlugins": [],
|
"optionalPlugins": [],
|
||||||
"requiredBundles": []
|
"requiredBundles": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||||
|
import { Query } from '@kbn/es-query';
|
||||||
|
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||||
|
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
|
import { useControlPanels } from '../hooks/use_control_panels';
|
||||||
|
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||||
|
|
||||||
|
interface CustomDatasetFiltersProps {
|
||||||
|
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomDatasetFilters = ({
|
||||||
|
logExplorerProfileStateService,
|
||||||
|
data,
|
||||||
|
}: CustomDatasetFiltersProps) => {
|
||||||
|
const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
||||||
|
logExplorerProfileStateService,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlGroupContainer>
|
||||||
|
<ControlGroupRenderer
|
||||||
|
ref={setControlGroupAPI}
|
||||||
|
getCreationOptions={getInitialInput}
|
||||||
|
query={query as Query}
|
||||||
|
filters={filters ?? []}
|
||||||
|
timeRange={timeRange}
|
||||||
|
/>
|
||||||
|
</ControlGroupContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlGroupContainer = euiStyled.div`
|
||||||
|
.controlGroup {
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.euiFormLabel {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.euiFormControlLayout {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default CustomDatasetFilters;
|
|
@ -11,14 +11,14 @@ import { DatasetsProvider, useDatasetsContext } from '../hooks/use_datasets';
|
||||||
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
||||||
import { IDatasetsClient } from '../services/datasets';
|
import { IDatasetsClient } from '../services/datasets';
|
||||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||||
import { useLogExplorerProfile } from '../hooks/use_log_explorer_profile';
|
import { useDatasetSelection } from '../hooks/use_dataset_selection';
|
||||||
|
|
||||||
interface CustomDatasetSelectorProps {
|
interface CustomDatasetSelectorProps {
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
||||||
const { datasetSelection, handleDatasetSelectionChange } = useLogExplorerProfile(
|
const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection(
|
||||||
logExplorerProfileStateService
|
logExplorerProfileStateService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,22 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
import type { CoreStart } from '@kbn/core/public';
|
import type { CoreStart } from '@kbn/core/public';
|
||||||
import { CustomizationCallback } from '@kbn/discover-plugin/public';
|
import { CustomizationCallback } from '@kbn/discover-plugin/public';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { dynamic } from '../utils/dynamic';
|
import { dynamic } from '../utils/dynamic';
|
||||||
|
|
||||||
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
||||||
|
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
|
||||||
|
|
||||||
interface CreateLogExplorerProfileCustomizationsDeps {
|
interface CreateLogExplorerProfileCustomizationsDeps {
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLogExplorerProfileCustomizations =
|
export const createLogExplorerProfileCustomizations =
|
||||||
({ core }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
({ core, data }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||||
async ({ customizations, stateContainer }) => {
|
async ({ customizations, stateContainer }) => {
|
||||||
// Lazy load dependencies
|
// Lazy load dependencies
|
||||||
const datasetServiceModuleLoadable = import('../services/datasets');
|
const datasetServiceModuleLoadable = import('../services/datasets');
|
||||||
|
@ -44,6 +47,7 @@ export const createLogExplorerProfileCustomizations =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
||||||
|
* Prepend the search bar with custom filter control groups depending on the selected dataset
|
||||||
*/
|
*/
|
||||||
customizations.set({
|
customizations.set({
|
||||||
id: 'search_bar',
|
id: 'search_bar',
|
||||||
|
@ -53,6 +57,12 @@ export const createLogExplorerProfileCustomizations =
|
||||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
logExplorerProfileStateService={logExplorerProfileStateService}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
PrependFilterBar: () => (
|
||||||
|
<LazyCustomDatasetFilters
|
||||||
|
logExplorerProfileStateService={logExplorerProfileStateService}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||||
|
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||||
|
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
|
import { Query, TimeRange } from '@kbn/es-query';
|
||||||
|
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||||
|
import { useSelector } from '@xstate/react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||||
|
|
||||||
|
export const useControlPanels = (
|
||||||
|
logExplorerProfileStateService: LogExplorerProfileStateService,
|
||||||
|
data: DataPublicPluginStart
|
||||||
|
) => {
|
||||||
|
const { query, filters, fromDate, toDate } = useQuerySubscriber({ data });
|
||||||
|
const timeRange: TimeRange = { from: fromDate!, to: toDate! };
|
||||||
|
|
||||||
|
const controlPanels = useSelector(logExplorerProfileStateService, (state) => {
|
||||||
|
if (!('controlPanels' in state.context)) return;
|
||||||
|
return state.context.controlPanels;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getInitialInput = useCallback(
|
||||||
|
async (initialInput: Partial<ControlGroupInput>) => {
|
||||||
|
const input: Partial<ControlGroupInput> = {
|
||||||
|
...initialInput,
|
||||||
|
viewMode: ViewMode.VIEW,
|
||||||
|
panels: controlPanels ?? initialInput.panels,
|
||||||
|
filters: filters ?? [],
|
||||||
|
query: query as Query,
|
||||||
|
timeRange: { from: fromDate!, to: toDate! },
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialInput: input };
|
||||||
|
},
|
||||||
|
[controlPanels, filters, fromDate, query, toDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setControlGroupAPI = useCallback(
|
||||||
|
(controlGroupAPI: ControlGroupAPI) => {
|
||||||
|
logExplorerProfileStateService.send({
|
||||||
|
type: 'INITIALIZE_CONTROL_GROUP_API',
|
||||||
|
controlGroupAPI,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[logExplorerProfileStateService]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { getInitialInput, setControlGroupAPI, query, filters, timeRange };
|
||||||
|
};
|
|
@ -10,13 +10,12 @@ import { useCallback } from 'react';
|
||||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||||
import { DatasetSelectionChange } from '../utils/dataset_selection';
|
import { DatasetSelectionChange } from '../utils/dataset_selection';
|
||||||
|
|
||||||
export const useLogExplorerProfile = (
|
export const useDatasetSelection = (
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService
|
logExplorerProfileStateService: LogExplorerProfileStateService
|
||||||
) => {
|
) => {
|
||||||
const datasetSelection = useSelector(
|
const datasetSelection = useSelector(logExplorerProfileStateService, (state) => {
|
||||||
logExplorerProfileStateService,
|
return state.context.datasetSelection;
|
||||||
(state) => state.context.datasetSelection
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
||||||
(data) => {
|
(data) => {
|
|
@ -28,10 +28,10 @@ export class DiscoverLogExplorerPlugin
|
||||||
public setup() {}
|
public setup() {}
|
||||||
|
|
||||||
public start(core: CoreStart, plugins: DiscoverLogExplorerStartDeps) {
|
public start(core: CoreStart, plugins: DiscoverLogExplorerStartDeps) {
|
||||||
const { discover } = plugins;
|
const { discover, data } = plugins;
|
||||||
|
|
||||||
discover.registerCustomizationProfile(LOG_EXPLORER_PROFILE_ID, {
|
discover.registerCustomizationProfile(LOG_EXPLORER_PROFILE_ID, {
|
||||||
customize: createLogExplorerProfileCustomizations({ core }),
|
customize: createLogExplorerProfileCustomizations({ core, data }),
|
||||||
deepLinks: [getLogExplorerDeepLink({ isVisible: this.config.featureFlags.deepLinkVisible })],
|
deepLinks: [getLogExplorerDeepLink({ isVisible: this.config.featureFlags.deepLinkVisible })],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllDatasetSelection } from '../../../utils/dataset_selection';
|
import { AllDatasetSelection } from '../../../utils/dataset_selection';
|
||||||
import { DefaultLogExplorerProfileState } from './types';
|
import { ControlPanels, DefaultLogExplorerProfileState } from './types';
|
||||||
|
|
||||||
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
||||||
datasetSelection: AllDatasetSelection.create(),
|
datasetSelection: AllDatasetSelection.create(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CONTROL_PANELS_URL_KEY = 'controlPanels';
|
||||||
|
|
||||||
|
export const availableControlsPanels = {
|
||||||
|
NAMESPACE: 'data_stream.namespace',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const controlPanelConfigs: ControlPanels = {
|
||||||
|
[availableControlsPanels.NAMESPACE]: {
|
||||||
|
order: 0,
|
||||||
|
width: 'medium',
|
||||||
|
grow: false,
|
||||||
|
type: 'optionsListControl',
|
||||||
|
explicitInput: {
|
||||||
|
id: availableControlsPanels.NAMESPACE,
|
||||||
|
fieldName: availableControlsPanels.NAMESPACE,
|
||||||
|
title: 'Namespace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const availableControlPanelFields = Object.values(availableControlsPanels);
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { IToasts } from '@kbn/core/public';
|
import { IToasts } from '@kbn/core/public';
|
||||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||||
import { actions, createMachine, interpret, InterpreterFrom } from 'xstate';
|
import { actions, createMachine, interpret, InterpreterFrom, raise } from 'xstate';
|
||||||
import { isDatasetSelection } from '../../../utils/dataset_selection';
|
import { isDatasetSelection } from '../../../utils/dataset_selection';
|
||||||
import { createAndSetDataView } from './data_view_service';
|
import { createAndSetDataView } from './data_view_service';
|
||||||
import { DEFAULT_CONTEXT } from './defaults';
|
import { DEFAULT_CONTEXT } from './defaults';
|
||||||
|
@ -15,18 +15,25 @@ import {
|
||||||
createCreateDataViewFailedNotifier,
|
createCreateDataViewFailedNotifier,
|
||||||
createDatasetSelectionRestoreFailedNotifier,
|
createDatasetSelectionRestoreFailedNotifier,
|
||||||
} from './notifications';
|
} from './notifications';
|
||||||
import type {
|
import {
|
||||||
|
ControlPanelRT,
|
||||||
LogExplorerProfileContext,
|
LogExplorerProfileContext,
|
||||||
LogExplorerProfileEvent,
|
LogExplorerProfileEvent,
|
||||||
LogExplorerProfileTypestate,
|
LogExplorerProfileTypeState,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { initializeFromUrl, listenUrlChange } from './url_state_storage_service';
|
import {
|
||||||
|
initializeControlPanels,
|
||||||
|
initializeFromUrl,
|
||||||
|
listenUrlChange,
|
||||||
|
subscribeControlGroup,
|
||||||
|
updateControlPanels,
|
||||||
|
} from './url_state_storage_service';
|
||||||
|
|
||||||
export const createPureLogExplorerProfileStateMachine = (
|
export const createPureLogExplorerProfileStateMachine = (
|
||||||
initialContext: LogExplorerProfileContext
|
initialContext: LogExplorerProfileContext
|
||||||
) =>
|
) =>
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCAFQ1AMwEs8wA6AVwDtrWAXagQz2oC9IAYgDaABgC6iUDlSxqnVMykgsiALQAmAMz0AHAE5dAFi0B2AKwAaEAE9EARi3n6+gGxHjZ8wF9v1tJi4BMRkFDR09Gzy3Lx8bFAAYhQAtgCqhHiCEIoMbABuqADWDAHY+EQk5FS0uRwx-PFJqGkZCPmoAMZcCsxi4n3KMnI9yqoI9qai9KZaokauuhbWdggaGrrT5vqmruauGj5+IKVBFaHVEVGcPA3MiSnpmSQUhPT43ZREyfQn5SFV4Vq0RucTuTRaeDazAKXR6fQGSBAQ2iilG6nsGns0127lEB0WBlErmWiDWGws2xxB18-nQZWClTCNUidRB8QAIt0uAA1ahgADuWRyLIKxR+dNO-yZl1ZsQ5XN5AqhMO61EU8Ikg1kKKUiLGjlM9HMGlcRIxogsWi0JIQ82cpnsunMxn0JtEWnWNOOEr+jIuQOucrunPYPL5gueRDeeA+X3FgV950BLOBQagIbDSvasLVvQkCOk2pGesQzjM9lEGN0WkWWlchl0NvM9n09Hsew0+n0ojmztdXt+DKTzKu9QEEEiEDoglSpHZAEEACoYAD6C8X84AyhhFyvt8gMABhRcASQA8gA5AtIou5tEIetTesY5vmC0mfTW2yILz0Vz2fZXEfdxdHbIwBx9IcARHWV+EgSdp3XLcdz3DAD2Pc8LxXDAACUcLPHDr2RYtQDGBZWzWXFTAsUwPy-FYjDceh3XdTsjCJDwwIghMoOlAMx3gxgcAgVVgwVcMhWYWpRRKSCzmgmVUzgichJEzgxNDRV+WVTpVXVfNNURYi7xLBAHQ2f8rS2UR9A-QwbU7Q0PDtD1tH2bQtG4+l5L4lNA2UphhNE9NxIFQRI1ed52E+QhvkHHz-T8gSVKC9SQs08MdJzfT+kMwthhM0jEEmLFtFEcw7VsrQu0bb9xkMFxu20EwLB7HtPK9ZhUAgOBlHiqV-S1ArUVMzR1jbcyljq7R6HmZyvC8yU-WTFhRxBSAhp1e81EdKYJkdKaVgxDQXAWo5+uWmClNBe5mkeTaSJURBP2Yt9TFAg4bXsCsXBAlrDlpHiEuTNa0wzLSHsKp6EFA6YtH+m06ymOYjA0SZqytK0TUWxMFP49aIEhkaioQGsNnMLQK3htYyUpr7vtmpr4a8Nr3Rx3jEtBgLqCnMAid1EmTFcI1KfdVGaerexEdNeg0crGZ2xA9t2eBq7-PHQK1PlDKBX5+8KzmZjqO2PFzFo6r6J-IC-1cSmjBbDjQIq3xfCAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCAFQ1AMwEs8wA6AVwDtrWAXagQz2oC9IAYgDaABgC6iUDlSxqnVMykgsiAIwAOAMz0NorVoDsAVi0a1x0QE4ATADYANCACe6zfVMbjeu-rWi7DQAWAF8QpzRMXAJiMgoaOno2eW5ePjYoADEKAFsAVUI8QQhFBjYAN1QAawZI7HwiEnIqWjKOVP4M7NR8woQK1ABjLgVmMXFx5Rk5UeVVBEsNekMrQK0bYKC7fztHF3VDII8vDTXDNUNtu2MwiPR6mKb41qT2nk7mLNyCopIKQno+BGlCIOXodWijTiLUSyU473Sn26vTw-WYlWGo3GkyQIGmKUUc0Qi2Wq20GyCWx2e1cCHM9CCXmuekMokO2y0txAEIasWaCTaKQRGQAIiMuAA1ahgADuxVKr0qNXB90hfOesLeaVF4qlsrRGJG1EU2IkU1kBKUuPmdhsTlpxjUR00Nis+g0NjUVis3i5PMe0IFryF2s+YvYkulcr+REBeGBoJVUV5Txhgvhoag4cj+oGmONYwkOOkFtm1uJdv2CDUFzsHkOjpshhsWguahsftVKcDLzhHURUAAwop2BQ8KQuMwwHhYPKp4rqrUuwH+b2tR8hyOxxOpzODUMjSai2bcfiy6B5qZjAyjIzDOYbNYm-bEFYDPRrJZNJpvI+NJ3kxXDV037DJh2YUdUHHSdp1nGMASBdgQUIMF-ShVdNRDDdwMg6Dd1gfd8yPCYTxLGYCyJBYtGvIJbw0e92yfQwXwQIJvXoN9jCsIIAiMV1XQAh50OA4MM34SB6AgcVYDAdgAGVpzAQZRiSCA6EEPJSBFABBAAVDAAH0dN07S5IwXSDLM5AMEHXSAEkAHkADlizxUsKPLatrCOZt720LRRFdIIbErWlmzUZZjEMaLNHMbRzkEtVUyDPsEQkqSIxk+TFOUgtVPU4zTPMyyMGs2zHKcgyACUMDk3SHJqgzMm0uzkDyGrXLPDyL3UbzlibDR-MC7iQpY8xDF0LjqNsIx6LURLuwwkC0ogSTpNkhS6FyxQmBwDKdQjPU5RKecBmVND1TTUT+3S9bsq2lTGD2o0w11KNCMPQsSMkU93MJTz21Megm18QJLnbRkaVfNQdFWURggMKKtnvBagKu1K0luzKNpyx7ns4V7DqjQR4LjBMUKTITLpS9cBFWjKuCyzalLx-bCZzGUPqxY8frIy1KPbdxLC0WxaLY8WghY20lmuBtRB41tHw7cJuWXYT0dpiTBi3KCAHEKCe2AmFYTWIEEOynLs+ztOQOyAC1DMHZzdKqhzkAM3XXc0gztNIOzOr+q0eurQGPEZCwBqbIJLhY-xWQ4rRGUpa5PXWVH1ZprC6fobWILHfXUEN-KwA0rS9IwEUjL07SDIlOyMAAdQD8j-uDmHLg4wLG3sN0rCdWPqKWGtKW9Aw+9tdPqbXLOtZ1vAC6L6g1JLzTjMd53Xfd0htKc0q5Ob-mAYMHRtiCYxAlOMwgn7qsazsCb9DsWjH3htRdkn5Lp7E7Pc9whecCNk9Nmm485QR3LBOcbQlRLkAhnL+N1Vq-3zgbABu1gE4W3DBPceZPqml5m5FuQcVBuBsNeYw4djCR0ODHW+noJqUN8GfeGoh9AGA-j2TC39Z6gPnigwB+MwJz3ATOEmhB-hkyQomC6n9OEIJznPf+-D0FCKwQRHB3NvrmkIQLUO5DzCUPvFHGhtIYb+HoLaOwWhdjGG8CYfQYQVbMFQBAOAyhpEcLAFow+wcAC0UMEA+OvN6YJITQmchVu4paxsMbiQgF488xDWKhTcPQow3Fz4rBhi2dhUSYkDmRD8eJ3VEkDV0IcKw4VAotkCCxbiH5r7aECI6YI2hQgRLVlPWRwp2ZHSKa3RJKwrAeD7jWfwidgqrFqUceijpWQ-m-PfHJIk8mCJ4cI+Av1tGeXsDLXwFhyGGGoosSWVZGRDMCByYKVwmxWCWRrGecTNneIGZSegFg7H8SbCYfxZ8T6nBhlct+Ny7B3Mzlw+md1mbbSIV1fp8wVg2DeVFSwnzornxYuQiKbYEUGBbIcUF8CVprWxvdFmeUl50D6UQ+YwRRBIo+bYL56KqwpzeTWUhpg-zUXCXcWBnTlqYwhSSqFrMXpZjerKKlAsthDLfNsUWdgKnXysCxQ5EVtjeFMOPDJbTeVUxkQK2J8ieGKKlZ5BF9KUWMrRf4qw2gGRsU0MyGGXhbntL5Qa66RKkF6z4dE02Zrg4+iWIcRk6xvQsI0G+WOfUbA8W0BcEaFhdWqw9R4r1grjV-z9RSzxTyEnzDfmY9JrZmyWPPtHWOnprzaFMEClOrSCVdMzT63hhdUFALFRgsBqjA2JNpZa4aIUbWxzfkMzQjIPTNjjefZWYQgA */
|
||||||
createMachine<LogExplorerProfileContext, LogExplorerProfileEvent, LogExplorerProfileTypestate>(
|
createMachine<LogExplorerProfileContext, LogExplorerProfileEvent, LogExplorerProfileTypeState>(
|
||||||
{
|
{
|
||||||
context: initialContext,
|
context: initialContext,
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
|
@ -53,7 +60,7 @@ export const createPureLogExplorerProfileStateMachine = (
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'createDataView',
|
src: 'createDataView',
|
||||||
onDone: {
|
onDone: {
|
||||||
target: 'initialized',
|
target: 'initializingControlPanels',
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
target: 'initialized',
|
target: 'initialized',
|
||||||
|
@ -61,33 +68,90 @@ export const createPureLogExplorerProfileStateMachine = (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
initializingControlPanels: {
|
||||||
|
invoke: {
|
||||||
|
src: 'initializeControlPanels',
|
||||||
|
onDone: {
|
||||||
|
target: 'initialized',
|
||||||
|
actions: ['storeControlPanels'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'initialized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
initialized: {
|
initialized: {
|
||||||
initial: 'idle',
|
type: 'parallel',
|
||||||
states: {
|
states: {
|
||||||
idle: {
|
datasetSelection: {
|
||||||
invoke: {
|
initial: 'idle',
|
||||||
src: 'listenUrlChange',
|
states: {
|
||||||
},
|
idle: {
|
||||||
on: {
|
invoke: {
|
||||||
UPDATE_DATASET_SELECTION: {
|
src: 'listenUrlChange',
|
||||||
target: 'updatingDataView',
|
},
|
||||||
actions: ['storeDatasetSelection'],
|
on: {
|
||||||
|
UPDATE_DATASET_SELECTION: {
|
||||||
|
target: 'updatingDataView',
|
||||||
|
actions: ['storeDatasetSelection'],
|
||||||
|
},
|
||||||
|
DATASET_SELECTION_RESTORE_FAILURE: {
|
||||||
|
target: 'updatingDataView',
|
||||||
|
actions: ['notifyDatasetSelectionRestoreFailed'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
DATASET_SELECTION_RESTORE_FAILURE: {
|
updatingDataView: {
|
||||||
target: 'updatingDataView',
|
invoke: {
|
||||||
actions: ['notifyDatasetSelectionRestoreFailed'],
|
src: 'createDataView',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: ['notifyDataViewUpdate'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: ['notifyCreateDataViewFailed'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatingDataView: {
|
controlGroups: {
|
||||||
invoke: {
|
initial: 'uninitialized',
|
||||||
src: 'createDataView',
|
states: {
|
||||||
onDone: {
|
uninitialized: {
|
||||||
target: 'idle',
|
on: {
|
||||||
|
INITIALIZE_CONTROL_GROUP_API: {
|
||||||
|
target: 'idle',
|
||||||
|
cond: 'controlGroupAPIExists',
|
||||||
|
actions: ['storeControlGroupAPI'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onError: {
|
idle: {
|
||||||
target: 'idle',
|
invoke: {
|
||||||
actions: ['notifyCreateDataViewFailed'],
|
src: 'subscribeControlGroup',
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
DATA_VIEW_UPDATED: {
|
||||||
|
target: 'updatingControlPanels',
|
||||||
|
},
|
||||||
|
UPDATE_CONTROL_PANELS: {
|
||||||
|
target: 'updatingControlPanels',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatingControlPanels: {
|
||||||
|
invoke: {
|
||||||
|
src: 'updateControlPanels',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: ['storeControlPanels'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,6 +168,26 @@ export const createPureLogExplorerProfileStateMachine = (
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
),
|
),
|
||||||
|
storeControlGroupAPI: actions.assign((_context, event) =>
|
||||||
|
'controlGroupAPI' in event
|
||||||
|
? {
|
||||||
|
controlGroupAPI: event.controlGroupAPI,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
),
|
||||||
|
storeControlPanels: actions.assign((_context, event) =>
|
||||||
|
'data' in event && ControlPanelRT.is(event.data)
|
||||||
|
? {
|
||||||
|
controlPanels: event.data,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
),
|
||||||
|
notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
controlGroupAPIExists: (_context, event) => {
|
||||||
|
return 'controlGroupAPI' in event && event.controlGroupAPI != null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -127,7 +211,10 @@ export const createLogExplorerProfileStateMachine = ({
|
||||||
services: {
|
services: {
|
||||||
createDataView: createAndSetDataView({ stateContainer }),
|
createDataView: createAndSetDataView({ stateContainer }),
|
||||||
initializeFromUrl: initializeFromUrl({ stateContainer }),
|
initializeFromUrl: initializeFromUrl({ stateContainer }),
|
||||||
|
initializeControlPanels: initializeControlPanels({ stateContainer }),
|
||||||
listenUrlChange: listenUrlChange({ stateContainer }),
|
listenUrlChange: listenUrlChange({ stateContainer }),
|
||||||
|
subscribeControlGroup: subscribeControlGroup({ stateContainer }),
|
||||||
|
updateControlPanels: updateControlPanels({ stateContainer }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as rt from 'io-ts';
|
||||||
|
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||||
import { DoneInvokeEvent } from 'xstate';
|
import { DoneInvokeEvent } from 'xstate';
|
||||||
import type { DatasetEncodingError, DatasetSelection } from '../../../utils/dataset_selection';
|
import type { DatasetEncodingError, DatasetSelection } from '../../../utils/dataset_selection';
|
||||||
|
|
||||||
|
@ -12,9 +14,17 @@ export interface WithDatasetSelection {
|
||||||
datasetSelection: DatasetSelection;
|
datasetSelection: DatasetSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WithControlPanelGroupAPI {
|
||||||
|
controlGroupAPI: ControlGroupAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithControlPanels {
|
||||||
|
controlPanels: ControlPanels;
|
||||||
|
}
|
||||||
|
|
||||||
export type DefaultLogExplorerProfileState = WithDatasetSelection;
|
export type DefaultLogExplorerProfileState = WithDatasetSelection;
|
||||||
|
|
||||||
export type LogExplorerProfileTypestate =
|
export type LogExplorerProfileTypeState =
|
||||||
| {
|
| {
|
||||||
value: 'uninitialized';
|
value: 'uninitialized';
|
||||||
context: WithDatasetSelection;
|
context: WithDatasetSelection;
|
||||||
|
@ -28,21 +38,37 @@ export type LogExplorerProfileTypestate =
|
||||||
context: WithDatasetSelection;
|
context: WithDatasetSelection;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
value: 'initialized';
|
value: 'initializingControlPanels';
|
||||||
context: WithDatasetSelection;
|
context: WithDatasetSelection;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
value: 'initialized';
|
||||||
|
context: WithDatasetSelection & WithControlPanels;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
value: 'initialized.idle';
|
value: 'initialized.idle';
|
||||||
context: WithDatasetSelection;
|
context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
value: 'initialized.updatingDataView';
|
value: 'initialized.updatingDataView';
|
||||||
context: WithDatasetSelection;
|
context: WithDatasetSelection;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'initialized.controlGroups.uninitialized';
|
||||||
|
context: WithDatasetSelection & WithControlPanels;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'initialized.controlGroups.idle';
|
||||||
|
context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'initialized.controlGroups.updatingControlPanels';
|
||||||
|
context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogExplorerProfileContext = LogExplorerProfileTypestate['context'];
|
export type LogExplorerProfileContext = LogExplorerProfileTypeState['context'];
|
||||||
|
|
||||||
export type LogExplorerProfileStateValue = LogExplorerProfileTypestate['value'];
|
export type LogExplorerProfileStateValue = LogExplorerProfileTypeState['value'];
|
||||||
|
|
||||||
export type LogExplorerProfileEvent =
|
export type LogExplorerProfileEvent =
|
||||||
| {
|
| {
|
||||||
|
@ -52,5 +78,36 @@ export type LogExplorerProfileEvent =
|
||||||
| {
|
| {
|
||||||
type: 'DATASET_SELECTION_RESTORE_FAILURE';
|
type: 'DATASET_SELECTION_RESTORE_FAILURE';
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'INITIALIZE_CONTROL_GROUP_API';
|
||||||
|
controlGroupAPI: ControlGroupAPI | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'UPDATE_CONTROL_PANELS';
|
||||||
|
controlPanels: ControlPanels | null;
|
||||||
|
}
|
||||||
|
| DoneInvokeEvent<DatasetSelection>
|
||||||
|
| DoneInvokeEvent<ControlPanels>
|
||||||
|
| DoneInvokeEvent<ControlGroupAPI>
|
||||||
| DoneInvokeEvent<DatasetEncodingError>
|
| DoneInvokeEvent<DatasetEncodingError>
|
||||||
| DoneInvokeEvent<Error>;
|
| DoneInvokeEvent<Error>;
|
||||||
|
|
||||||
|
const PanelRT = rt.type({
|
||||||
|
order: rt.number,
|
||||||
|
width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]),
|
||||||
|
grow: rt.boolean,
|
||||||
|
type: rt.string,
|
||||||
|
explicitInput: rt.intersection([
|
||||||
|
rt.type({ id: rt.string }),
|
||||||
|
rt.partial({
|
||||||
|
dataViewId: rt.string,
|
||||||
|
fieldName: rt.string,
|
||||||
|
title: rt.union([rt.string, rt.undefined]),
|
||||||
|
selectedOptions: rt.array(rt.string),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ControlPanelRT = rt.record(rt.string, PanelRT);
|
||||||
|
|
||||||
|
export type ControlPanels = rt.TypeOf<typeof ControlPanelRT>;
|
||||||
|
|
|
@ -5,14 +5,27 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import { InvokeCreator } from 'xstate';
|
import { InvokeCreator } from 'xstate';
|
||||||
|
import { pick, mapValues } from 'lodash';
|
||||||
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||||
|
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
import {
|
import {
|
||||||
AllDatasetSelection,
|
AllDatasetSelection,
|
||||||
decodeDatasetSelectionId,
|
decodeDatasetSelectionId,
|
||||||
hydrateDatasetSelection,
|
hydrateDatasetSelection,
|
||||||
isDatasetSelection,
|
isDatasetSelection,
|
||||||
} from '../../../utils/dataset_selection';
|
} from '../../../utils/dataset_selection';
|
||||||
import type { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
import {
|
||||||
|
ControlPanelRT,
|
||||||
|
ControlPanels,
|
||||||
|
LogExplorerProfileContext,
|
||||||
|
LogExplorerProfileEvent,
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
availableControlPanelFields,
|
||||||
|
controlPanelConfigs,
|
||||||
|
CONTROL_PANELS_URL_KEY,
|
||||||
|
} from './defaults';
|
||||||
|
|
||||||
interface LogExplorerProfileUrlStateDependencies {
|
interface LogExplorerProfileUrlStateDependencies {
|
||||||
stateContainer: DiscoverStateContainer;
|
stateContainer: DiscoverStateContainer;
|
||||||
|
@ -61,6 +74,20 @@ export const initializeFromUrl =
|
||||||
return extractDatasetSelectionFromIndex({ index, context });
|
return extractDatasetSelectionFromIndex({ index, context });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initializeControlPanels =
|
||||||
|
({
|
||||||
|
stateContainer,
|
||||||
|
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||||
|
LogExplorerProfileContext,
|
||||||
|
LogExplorerProfileEvent
|
||||||
|
> =>
|
||||||
|
async (context) => {
|
||||||
|
const urlPanels = stateContainer.stateStorage.get<ControlPanels>(CONTROL_PANELS_URL_KEY);
|
||||||
|
const controlPanelsWithId = constructControlPanelsWithDataViewId(stateContainer, urlPanels!);
|
||||||
|
|
||||||
|
return controlPanelsWithId;
|
||||||
|
};
|
||||||
|
|
||||||
const extractDatasetSelectionFromIndex = ({
|
const extractDatasetSelectionFromIndex = ({
|
||||||
index,
|
index,
|
||||||
context,
|
context,
|
||||||
|
@ -78,3 +105,134 @@ const extractDatasetSelectionFromIndex = ({
|
||||||
|
|
||||||
return datasetSelection;
|
return datasetSelection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subscribeControlGroup =
|
||||||
|
({
|
||||||
|
stateContainer,
|
||||||
|
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||||
|
LogExplorerProfileContext,
|
||||||
|
LogExplorerProfileEvent
|
||||||
|
> =>
|
||||||
|
(context) =>
|
||||||
|
(send) => {
|
||||||
|
if (!('controlGroupAPI' in context)) return;
|
||||||
|
|
||||||
|
const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe(
|
||||||
|
(newFilters) => {
|
||||||
|
stateContainer.internalState.transitions.setCustomFilters(newFilters);
|
||||||
|
stateContainer.actions.fetchData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keeps our state in sync with the url changes and makes sure it adheres to correct schema
|
||||||
|
const urlSubscription = stateContainer.stateStorage
|
||||||
|
.change$<ControlPanels>(CONTROL_PANELS_URL_KEY)
|
||||||
|
.subscribe((controlPanels) => {
|
||||||
|
if (!deepEqual(controlPanels, context.controlPanels)) {
|
||||||
|
send({ type: 'UPDATE_CONTROL_PANELS', controlPanels });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keeps the url in sync with the controls state after change
|
||||||
|
const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => {
|
||||||
|
if (!deepEqual(panels, context.controlPanels)) {
|
||||||
|
send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
filtersSubscription.unsubscribe();
|
||||||
|
urlSubscription.unsubscribe();
|
||||||
|
inputSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateControlPanels =
|
||||||
|
({
|
||||||
|
stateContainer,
|
||||||
|
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||||
|
LogExplorerProfileContext,
|
||||||
|
LogExplorerProfileEvent
|
||||||
|
> =>
|
||||||
|
async (context, event) => {
|
||||||
|
if (!('controlGroupAPI' in context)) return;
|
||||||
|
|
||||||
|
const newControlPanels =
|
||||||
|
('controlPanels' in event && event.controlPanels) || context.controlPanels;
|
||||||
|
const controlPanelsWithId = constructControlPanelsWithDataViewId(
|
||||||
|
stateContainer,
|
||||||
|
newControlPanels!
|
||||||
|
);
|
||||||
|
|
||||||
|
context.controlGroupAPI.updateInput({ panels: controlPanelsWithId });
|
||||||
|
|
||||||
|
return controlPanelsWithId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
const constructControlPanelsWithDataViewId = (
|
||||||
|
stateContainer: DiscoverStateContainer,
|
||||||
|
newControlPanels: ControlPanels
|
||||||
|
) => {
|
||||||
|
const dataView = stateContainer.internalState.getState().dataView!;
|
||||||
|
|
||||||
|
const validatedControlPanels = isValidState(newControlPanels)
|
||||||
|
? newControlPanels
|
||||||
|
: getVisibleControlPanelsConfig(dataView);
|
||||||
|
|
||||||
|
const controlsPanelsWithId = mergeDefaultPanelsWithUrlConfig(dataView, validatedControlPanels!);
|
||||||
|
|
||||||
|
stateContainer.stateStorage.set(CONTROL_PANELS_URL_KEY, cleanControlPanels(controlsPanelsWithId));
|
||||||
|
|
||||||
|
return controlsPanelsWithId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidState = (state: ControlPanels | undefined | null): boolean => {
|
||||||
|
return Object.keys(state ?? {}).length > 0 && ControlPanelRT.is(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVisibleControlPanels = (dataView: DataView | undefined) =>
|
||||||
|
availableControlPanelFields.filter(
|
||||||
|
(panelKey) => dataView?.fields.getByName(panelKey) !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getVisibleControlPanelsConfig = (dataView?: DataView) => {
|
||||||
|
return getVisibleControlPanels(dataView).reduce((panelsMap, panelKey) => {
|
||||||
|
const config = controlPanelConfigs[panelKey];
|
||||||
|
|
||||||
|
return { ...panelsMap, [panelKey]: config };
|
||||||
|
}, {} as ControlPanels);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => {
|
||||||
|
return mapValues(controlPanels, (controlPanelConfig) => ({
|
||||||
|
...controlPanelConfig,
|
||||||
|
explicitInput: { ...controlPanelConfig.explicitInput, dataViewId },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanControlPanels = (controlPanels: ControlPanels) => {
|
||||||
|
return mapValues(controlPanels, (controlPanelConfig) => {
|
||||||
|
const { explicitInput } = controlPanelConfig;
|
||||||
|
const { dataViewId, ...rest } = explicitInput;
|
||||||
|
return { ...controlPanelConfig, explicitInput: rest };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeDefaultPanelsWithUrlConfig = (dataView: DataView, urlPanels: ControlPanels) => {
|
||||||
|
// Get default panel configs from existing fields in data view
|
||||||
|
const visiblePanels = getVisibleControlPanelsConfig(dataView);
|
||||||
|
|
||||||
|
// Get list of panel which can be overridden to avoid merging additional config from url
|
||||||
|
const existingKeys = Object.keys(visiblePanels);
|
||||||
|
const controlPanelsToOverride = pick(urlPanels, existingKeys);
|
||||||
|
|
||||||
|
// Merge default and existing configs and add dataView.id to each of them
|
||||||
|
return addDataViewIdToControlPanels(
|
||||||
|
{ ...visiblePanels, ...controlPanelsToOverride },
|
||||||
|
dataView.id
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
import { DiscoverStart } from '@kbn/discover-plugin/public';
|
import { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||||
|
|
||||||
export type DiscoverLogExplorerPluginSetup = void;
|
export type DiscoverLogExplorerPluginSetup = void;
|
||||||
export type DiscoverLogExplorerPluginStart = void;
|
export type DiscoverLogExplorerPluginStart = void;
|
||||||
|
|
||||||
export interface DiscoverLogExplorerStartDeps {
|
export interface DiscoverLogExplorerStartDeps {
|
||||||
|
data: DataPublicPluginStart;
|
||||||
discover: DiscoverStart;
|
discover: DiscoverStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
"@kbn/io-ts-utils",
|
"@kbn/io-ts-utils",
|
||||||
"@kbn/data-views-plugin",
|
"@kbn/data-views-plugin",
|
||||||
"@kbn/rison",
|
"@kbn/rison",
|
||||||
|
"@kbn/controls-plugin",
|
||||||
|
"@kbn/embeddable-plugin",
|
||||||
|
"@kbn/es-query",
|
||||||
|
"@kbn/kibana-react-plugin",
|
||||||
|
"@kbn/data-plugin",
|
||||||
|
"@kbn/unified-field-list",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": ["target/**/*"]
|
||||||
|
|
|
@ -61,7 +61,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
const azureActivitylogsIndex =
|
const azureActivitylogsIndex =
|
||||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||||
await PageObjects.common.navigateToApp('discover', {
|
await PageObjects.common.navigateToApp('discover', {
|
||||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(
|
||||||
|
azureActivitylogsIndex
|
||||||
|
)})&controlPanels=()`,
|
||||||
});
|
});
|
||||||
const azureDatasetSelectionTitle =
|
const azureDatasetSelectionTitle =
|
||||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||||
|
|
|
@ -61,7 +61,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
const azureActivitylogsIndex =
|
const azureActivitylogsIndex =
|
||||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||||
await PageObjects.common.navigateToApp('discover', {
|
await PageObjects.common.navigateToApp('discover', {
|
||||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(
|
||||||
|
azureActivitylogsIndex
|
||||||
|
)})&controlPanels=()`,
|
||||||
});
|
});
|
||||||
const azureDatasetSelectionTitle =
|
const azureDatasetSelectionTitle =
|
||||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue