mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -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",
|
||||
"server": false,
|
||||
"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 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 {
|
||||
|
@ -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');
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
"@kbn/core",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -144,4 +144,10 @@ export const optionsListReducers = {
|
|||
) => {
|
||||
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
|
||||
*/
|
||||
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(
|
||||
|
@ -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,
|
||||
|
|
|
@ -234,6 +234,11 @@ export const DiscoverTopNav = ({
|
|||
textBasedLanguageModeErrors ? [textBasedLanguageModeErrors] : undefined
|
||||
}
|
||||
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 { 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$;
|
||||
|
@ -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 {
|
||||
|
@ -213,6 +216,7 @@ export function getDataStateContainer({
|
|||
searchSessionId,
|
||||
services,
|
||||
getAppState,
|
||||
getInternalState,
|
||||
savedSearch: getSavedSearch(),
|
||||
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ 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 {
|
||||
|
@ -19,6 +20,7 @@ export interface InternalState {
|
|||
savedDataViews: DataViewListItem[];
|
||||
adHocDataViews: DataView[];
|
||||
expandedDoc: DataTableRecord | undefined;
|
||||
customFilters: Filter[];
|
||||
}
|
||||
|
||||
interface InternalStateTransitions {
|
||||
|
@ -35,6 +37,7 @@ interface InternalStateTransitions {
|
|||
setExpandedDoc: (
|
||||
state: InternalState
|
||||
) => (dataView: DataTableRecord | undefined) => InternalState;
|
||||
setCustomFilters: (state: InternalState) => (customFilters: Filter[]) => InternalState;
|
||||
}
|
||||
|
||||
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
|
||||
|
@ -52,6 +55,7 @@ export function getInternalStateContainer() {
|
|||
adHocDataViews: [],
|
||||
savedDataViews: [],
|
||||
expandedDoc: undefined,
|
||||
customFilters: [],
|
||||
},
|
||||
{
|
||||
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
|
||||
|
@ -97,6 +101,10 @@ export function getInternalStateContainer() {
|
|||
...prevState,
|
||||
expandedDoc,
|
||||
}),
|
||||
setCustomFilters: (prevState: InternalState) => (customFilters: Filter[]) => ({
|
||||
...prevState,
|
||||
customFilters,
|
||||
}),
|
||||
},
|
||||
{},
|
||||
{ freeze: (state) => state }
|
||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { History } from 'history';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
IKbnUrlStateStorage,
|
||||
StateContainer,
|
||||
withNotifyOnErrors,
|
||||
} from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -252,6 +257,7 @@ export function getDiscoverStateContainer({
|
|||
services,
|
||||
searchSessionManager,
|
||||
getAppState: appStateContainer.getState,
|
||||
getInternalState: internalStateContainer.getState,
|
||||
getSavedSearch: savedSearchContainer.getState,
|
||||
setDataView,
|
||||
});
|
||||
|
@ -451,6 +457,7 @@ export function getDiscoverStateContainer({
|
|||
internalState: internalStateContainer,
|
||||
dataState: dataStateContainer,
|
||||
savedSearchState: savedSearchContainer,
|
||||
stateStorage,
|
||||
searchSessionManager,
|
||||
actions: {
|
||||
initializeAndSync,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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();
|
||||
|
|
|
@ -26,10 +26,12 @@ import { FetchStatus } from '../../types';
|
|||
import { DataMsg, RecordRawType, SavedSearchData } from '../services/discover_data_state_container';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { fetchSql } from './fetch_sql';
|
||||
import { InternalState } from '../services/discover_internal_state_container';
|
||||
|
||||
export interface FetchDeps {
|
||||
abortController: AbortController;
|
||||
getAppState: () => DiscoverAppState;
|
||||
getInternalState: () => InternalState;
|
||||
initialFetchStatus: FetchStatus;
|
||||
inspectorAdapters: Adapters;
|
||||
savedSearch: SavedSearch;
|
||||
|
@ -50,7 +52,14 @@ export function fetchAll(
|
|||
reset = false,
|
||||
fetchDeps: FetchDeps
|
||||
): Promise<void> {
|
||||
const { initialFetchStatus, getAppState, services, inspectorAdapters, savedSearch } = fetchDeps;
|
||||
const {
|
||||
initialFetchStatus,
|
||||
getAppState,
|
||||
getInternalState,
|
||||
services,
|
||||
inspectorAdapters,
|
||||
savedSearch,
|
||||
} = fetchDeps;
|
||||
const { data } = services;
|
||||
const searchSource = savedSearch.searchSource.createChild();
|
||||
|
||||
|
@ -70,6 +79,7 @@ export function fetchAll(
|
|||
dataView,
|
||||
services,
|
||||
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 { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
const getUiSettingsMock = (value: boolean) => {
|
||||
return {
|
||||
|
@ -27,6 +28,7 @@ describe('updateVolatileSearchSource', () => {
|
|||
dataView: dataViewMock,
|
||||
services: discoverServiceMock,
|
||||
sort: [] as SortOrder[],
|
||||
customFilters: [],
|
||||
});
|
||||
expect(searchSource.getField('fields')).toBe(undefined);
|
||||
});
|
||||
|
@ -38,6 +40,7 @@ describe('updateVolatileSearchSource', () => {
|
|||
dataView: dataViewMock,
|
||||
services: discoverServiceMock,
|
||||
sort: [] as SortOrder[],
|
||||
customFilters: [],
|
||||
});
|
||||
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
|
||||
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
|
||||
|
@ -50,6 +53,7 @@ describe('updateVolatileSearchSource', () => {
|
|||
dataView: dataViewMock,
|
||||
services: discoverServiceMock,
|
||||
sort: [] as SortOrder[],
|
||||
customFilters: [],
|
||||
});
|
||||
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
||||
{ field: '*', include_unmapped: 'true' },
|
||||
|
@ -64,8 +68,25 @@ describe('updateVolatileSearchSource', () => {
|
|||
dataView: dataViewMock,
|
||||
services: discoverServiceMock,
|
||||
sort: [] as SortOrder[],
|
||||
customFilters: [],
|
||||
});
|
||||
expect(volatileSearchSourceMock.getField('fields')).toEqual(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 { DataViewType, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
|
@ -22,10 +23,12 @@ export function updateVolatileSearchSource(
|
|||
dataView,
|
||||
services,
|
||||
sort,
|
||||
customFilters,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
services: DiscoverServices;
|
||||
sort?: SortOrder[];
|
||||
customFilters: Filter[];
|
||||
}
|
||||
) {
|
||||
const { uiSettings, data } = services;
|
||||
|
@ -37,11 +40,16 @@ export function updateVolatileSearchSource(
|
|||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
searchSource.setField('trackTotalHits', true).setField('sort', usedSort);
|
||||
|
||||
let filters = [...customFilters];
|
||||
|
||||
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
|
||||
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) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
const fields: Record<string, string> = { field: '*' };
|
||||
|
|
|
@ -13,5 +13,6 @@ import type { ComponentType, ReactElement } from 'react';
|
|||
export interface SearchBarCustomization {
|
||||
id: 'search_bar';
|
||||
CustomDataViewPicker?: ComponentType;
|
||||
PrependFilterBar?: ComponentType;
|
||||
CustomSearchBar?: (props: TopNavMenuProps<AggregateQuery>) => ReactElement;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { buildExistsFilter } from '@kbn/es-query';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { SearchBar, SearchBarProps } from '../search_bar';
|
||||
import { setIndexPatterns } from '../services';
|
||||
|
||||
|
@ -454,6 +455,23 @@ storiesOf('SearchBar', module)
|
|||
},
|
||||
} 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', () =>
|
||||
wrapSearchBarInContext({
|
||||
disableQueryLanguageSwitcher: true,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { EuiFlexGroup, useEuiTheme } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n-react';
|
||||
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 FilterItems, { type FilterItemsProps } from './filter_item/filter_items';
|
||||
|
||||
|
@ -33,6 +33,10 @@ export interface Props {
|
|||
* Disable all interactive actions
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
/**
|
||||
* Prepends custom filter controls to the search bar
|
||||
*/
|
||||
prepend?: ReactNode;
|
||||
/** Array of suggestion abstraction that controls the render of the field */
|
||||
suggestionsAbstraction?: SuggestionsAbstraction;
|
||||
}
|
||||
|
@ -52,6 +56,7 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) {
|
|||
alignItems="center"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{props.prepend}
|
||||
<FilterItems
|
||||
filters={props.filters!}
|
||||
onFiltersUpdated={props.onFiltersUpdated}
|
||||
|
|
|
@ -252,6 +252,7 @@ export function createSearchBar({
|
|||
isScreenshotMode={isScreenshotMode}
|
||||
dataTestSubj={props.dataTestSubj}
|
||||
filtersForSuggestions={props.filtersForSuggestions}
|
||||
prependFilterBar={props.prependFilterBar}
|
||||
/>
|
||||
</core.i18n.Context>
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -60,6 +60,7 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
|
|||
filters?: Filter[];
|
||||
filtersForSuggestions?: Filter[];
|
||||
hiddenFilterPanelOptions?: QueryBarMenuProps['hiddenPanelOptions'];
|
||||
prependFilterBar?: React.ReactNode;
|
||||
// Date picker
|
||||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
|
@ -548,6 +549,7 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
|
|||
hiddenPanelOptions={this.props.hiddenFilterPanelOptions}
|
||||
isDisabled={this.props.isDisabled}
|
||||
data-test-subj="unifiedFilterBar"
|
||||
prepend={this.props.prependFilterBar}
|
||||
suggestionsAbstraction={this.props.suggestionsAbstraction}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
const kibanaServer = getService('kibanaServer');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const browser = getService('browser');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const defaultSettings = { defaultIndex: 'logstash-*' };
|
||||
|
||||
describe('Customizations', () => {
|
||||
|
@ -68,5 +69,33 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
expect(title).to.eql(expected.title);
|
||||
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,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "discoverLogExplorer"],
|
||||
"requiredPlugins": ["discover", "fleet", "kibanaReact", "kibanaUtils"],
|
||||
"requiredPlugins": ["data", "dataViews", "discover", "fleet", "kibanaReact", "kibanaUtils", "controls", "embeddable"],
|
||||
"optionalPlugins": [],
|
||||
"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 { IDatasetsClient } from '../services/datasets';
|
||||
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 {
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||
}
|
||||
|
||||
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
||||
const { datasetSelection, handleDatasetSelectionChange } = useLogExplorerProfile(
|
||||
const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection(
|
||||
logExplorerProfileStateService
|
||||
);
|
||||
|
||||
|
|
|
@ -5,19 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { CustomizationCallback } from '@kbn/discover-plugin/public';
|
||||
import React from 'react';
|
||||
import { dynamic } from '../utils/dynamic';
|
||||
|
||||
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
||||
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
|
||||
|
||||
interface CreateLogExplorerProfileCustomizationsDeps {
|
||||
core: CoreStart;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
export const createLogExplorerProfileCustomizations =
|
||||
({ core }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||
({ core, data }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||
async ({ customizations, stateContainer }) => {
|
||||
// Lazy load dependencies
|
||||
const datasetServiceModuleLoadable = import('../services/datasets');
|
||||
|
@ -44,6 +47,7 @@ export const createLogExplorerProfileCustomizations =
|
|||
|
||||
/**
|
||||
* 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({
|
||||
id: 'search_bar',
|
||||
|
@ -53,6 +57,12 @@ export const createLogExplorerProfileCustomizations =
|
|||
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 { DatasetSelectionChange } from '../utils/dataset_selection';
|
||||
|
||||
export const useLogExplorerProfile = (
|
||||
export const useDatasetSelection = (
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService
|
||||
) => {
|
||||
const datasetSelection = useSelector(
|
||||
logExplorerProfileStateService,
|
||||
(state) => state.context.datasetSelection
|
||||
);
|
||||
const datasetSelection = useSelector(logExplorerProfileStateService, (state) => {
|
||||
return state.context.datasetSelection;
|
||||
});
|
||||
|
||||
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
||||
(data) => {
|
|
@ -28,10 +28,10 @@ export class DiscoverLogExplorerPlugin
|
|||
public setup() {}
|
||||
|
||||
public start(core: CoreStart, plugins: DiscoverLogExplorerStartDeps) {
|
||||
const { discover } = plugins;
|
||||
const { discover, data } = plugins;
|
||||
|
||||
discover.registerCustomizationProfile(LOG_EXPLORER_PROFILE_ID, {
|
||||
customize: createLogExplorerProfileCustomizations({ core }),
|
||||
customize: createLogExplorerProfileCustomizations({ core, data }),
|
||||
deepLinks: [getLogExplorerDeepLink({ isVisible: this.config.featureFlags.deepLinkVisible })],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,8 +6,30 @@
|
|||
*/
|
||||
|
||||
import { AllDatasetSelection } from '../../../utils/dataset_selection';
|
||||
import { DefaultLogExplorerProfileState } from './types';
|
||||
import { ControlPanels, DefaultLogExplorerProfileState } from './types';
|
||||
|
||||
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
||||
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 { 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 { createAndSetDataView } from './data_view_service';
|
||||
import { DEFAULT_CONTEXT } from './defaults';
|
||||
|
@ -15,18 +15,25 @@ import {
|
|||
createCreateDataViewFailedNotifier,
|
||||
createDatasetSelectionRestoreFailedNotifier,
|
||||
} from './notifications';
|
||||
import type {
|
||||
import {
|
||||
ControlPanelRT,
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent,
|
||||
LogExplorerProfileTypestate,
|
||||
LogExplorerProfileTypeState,
|
||||
} from './types';
|
||||
import { initializeFromUrl, listenUrlChange } from './url_state_storage_service';
|
||||
import {
|
||||
initializeControlPanels,
|
||||
initializeFromUrl,
|
||||
listenUrlChange,
|
||||
subscribeControlGroup,
|
||||
updateControlPanels,
|
||||
} from './url_state_storage_service';
|
||||
|
||||
export const createPureLogExplorerProfileStateMachine = (
|
||||
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 */
|
||||
createMachine<LogExplorerProfileContext, LogExplorerProfileEvent, LogExplorerProfileTypestate>(
|
||||
/** @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>(
|
||||
{
|
||||
context: initialContext,
|
||||
predictableActionArguments: true,
|
||||
|
@ -53,7 +60,7 @@ export const createPureLogExplorerProfileStateMachine = (
|
|||
invoke: {
|
||||
src: 'createDataView',
|
||||
onDone: {
|
||||
target: 'initialized',
|
||||
target: 'initializingControlPanels',
|
||||
},
|
||||
onError: {
|
||||
target: 'initialized',
|
||||
|
@ -61,33 +68,90 @@ export const createPureLogExplorerProfileStateMachine = (
|
|||
},
|
||||
},
|
||||
},
|
||||
initializingControlPanels: {
|
||||
invoke: {
|
||||
src: 'initializeControlPanels',
|
||||
onDone: {
|
||||
target: 'initialized',
|
||||
actions: ['storeControlPanels'],
|
||||
},
|
||||
onError: {
|
||||
target: 'initialized',
|
||||
},
|
||||
},
|
||||
},
|
||||
initialized: {
|
||||
initial: 'idle',
|
||||
type: 'parallel',
|
||||
states: {
|
||||
idle: {
|
||||
invoke: {
|
||||
src: 'listenUrlChange',
|
||||
},
|
||||
on: {
|
||||
UPDATE_DATASET_SELECTION: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['storeDatasetSelection'],
|
||||
datasetSelection: {
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
invoke: {
|
||||
src: 'listenUrlChange',
|
||||
},
|
||||
on: {
|
||||
UPDATE_DATASET_SELECTION: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['storeDatasetSelection'],
|
||||
},
|
||||
DATASET_SELECTION_RESTORE_FAILURE: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['notifyDatasetSelectionRestoreFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
DATASET_SELECTION_RESTORE_FAILURE: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['notifyDatasetSelectionRestoreFailed'],
|
||||
updatingDataView: {
|
||||
invoke: {
|
||||
src: 'createDataView',
|
||||
onDone: {
|
||||
target: 'idle',
|
||||
actions: ['notifyDataViewUpdate'],
|
||||
},
|
||||
onError: {
|
||||
target: 'idle',
|
||||
actions: ['notifyCreateDataViewFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatingDataView: {
|
||||
invoke: {
|
||||
src: 'createDataView',
|
||||
onDone: {
|
||||
target: 'idle',
|
||||
controlGroups: {
|
||||
initial: 'uninitialized',
|
||||
states: {
|
||||
uninitialized: {
|
||||
on: {
|
||||
INITIALIZE_CONTROL_GROUP_API: {
|
||||
target: 'idle',
|
||||
cond: 'controlGroupAPIExists',
|
||||
actions: ['storeControlGroupAPI'],
|
||||
},
|
||||
},
|
||||
},
|
||||
onError: {
|
||||
target: 'idle',
|
||||
actions: ['notifyCreateDataViewFailed'],
|
||||
idle: {
|
||||
invoke: {
|
||||
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: {
|
||||
createDataView: createAndSetDataView({ stateContainer }),
|
||||
initializeFromUrl: initializeFromUrl({ stateContainer }),
|
||||
initializeControlPanels: initializeControlPanels({ stateContainer }),
|
||||
listenUrlChange: listenUrlChange({ stateContainer }),
|
||||
subscribeControlGroup: subscribeControlGroup({ stateContainer }),
|
||||
updateControlPanels: updateControlPanels({ stateContainer }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||
import { DoneInvokeEvent } from 'xstate';
|
||||
import type { DatasetEncodingError, DatasetSelection } from '../../../utils/dataset_selection';
|
||||
|
||||
|
@ -12,9 +14,17 @@ export interface WithDatasetSelection {
|
|||
datasetSelection: DatasetSelection;
|
||||
}
|
||||
|
||||
export interface WithControlPanelGroupAPI {
|
||||
controlGroupAPI: ControlGroupAPI;
|
||||
}
|
||||
|
||||
export interface WithControlPanels {
|
||||
controlPanels: ControlPanels;
|
||||
}
|
||||
|
||||
export type DefaultLogExplorerProfileState = WithDatasetSelection;
|
||||
|
||||
export type LogExplorerProfileTypestate =
|
||||
export type LogExplorerProfileTypeState =
|
||||
| {
|
||||
value: 'uninitialized';
|
||||
context: WithDatasetSelection;
|
||||
|
@ -28,21 +38,37 @@ export type LogExplorerProfileTypestate =
|
|||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initialized';
|
||||
value: 'initializingControlPanels';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initialized';
|
||||
context: WithDatasetSelection & WithControlPanels;
|
||||
}
|
||||
| {
|
||||
value: 'initialized.idle';
|
||||
context: WithDatasetSelection;
|
||||
context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels;
|
||||
}
|
||||
| {
|
||||
value: 'initialized.updatingDataView';
|
||||
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 =
|
||||
| {
|
||||
|
@ -52,5 +78,36 @@ export type LogExplorerProfileEvent =
|
|||
| {
|
||||
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<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.
|
||||
*/
|
||||
import { InvokeCreator } from 'xstate';
|
||||
import { pick, mapValues } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
AllDatasetSelection,
|
||||
decodeDatasetSelectionId,
|
||||
hydrateDatasetSelection,
|
||||
isDatasetSelection,
|
||||
} 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 {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
|
@ -61,6 +74,20 @@ export const initializeFromUrl =
|
|||
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 = ({
|
||||
index,
|
||||
context,
|
||||
|
@ -78,3 +105,134 @@ const extractDatasetSelectionFromIndex = ({
|
|||
|
||||
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.
|
||||
*/
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
|
||||
export type DiscoverLogExplorerPluginSetup = void;
|
||||
export type DiscoverLogExplorerPluginStart = void;
|
||||
|
||||
export interface DiscoverLogExplorerStartDeps {
|
||||
data: DataPublicPluginStart;
|
||||
discover: DiscoverStart;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
"@kbn/io-ts-utils",
|
||||
"@kbn/data-views-plugin",
|
||||
"@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",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
|
|
|
@ -61,7 +61,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(
|
||||
azureActivitylogsIndex
|
||||
)})&controlPanels=()`,
|
||||
});
|
||||
const azureDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
|
|
@ -61,7 +61,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(
|
||||
azureActivitylogsIndex
|
||||
)})&controlPanels=()`,
|
||||
});
|
||||
const azureDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue