mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Logs+] Restore Dataset selection from page URL (#161144)
## 📓 Summary Closes #160425 After the [first implementation of the log-explorer profile](https://github.com/elastic/kibana/pull/159907), we wanted to restore the selection of the dataset for a user when landing on the Discover log-explorer profile. Since we create an ad-hoc data view for Discover starting from the dataset details, we needed to develop a system for intercepting the `index` query parameter (which is used by Discover as the source of truth for restoring a data view), create our ad-hoc data view and store in the URL an encoded ID with the required details to restore the selection. The following video shows the user journey for: - Landing on the log-explorer profile with no index param, nothing to restore and fallback to All log datasets. - Landing on the log-explorer profile invalid index param, notify about failure and fallback to All log datasets. - Select a different dataset, applies the new data view and update the URL. When the URL is accessed directly, restore and initialize the data view for the selection. - Navigate back and forth in the browser history, restoring the selection and data view on `index` param changes.37a212ee
-08e4-4e54-8e42-1d739c38f164 ## 💡 Reviewer hints To have better control over the page selection and the restore process, we prepared the DatasetSelector component for [being controlled by the parent component](https://github.com/elastic/kibana/pull/160971). Having that ready, we now implemented a new top-level state machine with the following responsibilities: - Re-initialize (decompress/decode) the dataset selection from the `index` query params. - Derive and set into Discover state a new ad-hoc data view. - Keep track of new dataset selection changes and update the URL state and the current data view. <img width="1224" alt="log-explorer-machine" src="67e3ff17
-dc3f-4dcf-b6c0-f40dbbea2d44"> We found a race condition between the Discover URL initialization + data view initialization against the log-explorer profile customizations being applied. To guarantee we correctly initialize the state machine and restore the selection before Discover goes through its initialization steps, we need to wait for the customization service to exist in Discover so that also the customization callbacks are successfully invoked. --------- Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a3e1aab363
commit
446157f6d0
36 changed files with 861 additions and 158 deletions
|
@ -178,7 +178,7 @@ export const DiscoverTopNav = ({
|
|||
currentDataViewId: dataView?.id,
|
||||
onAddField: addField,
|
||||
onDataViewCreated: createNewDataView,
|
||||
onCreateDefaultAdHocDataView: stateContainer.actions.onCreateDefaultAdHocDataView,
|
||||
onCreateDefaultAdHocDataView: stateContainer.actions.createAndAppendAdHocDataView,
|
||||
onChangeDataView: stateContainer.actions.onChangeDataView,
|
||||
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
|
||||
adHocDataViews,
|
||||
|
|
|
@ -27,7 +27,10 @@ jest.mock('../../customizations', () => {
|
|||
const originalModule = jest.requireActual('../../customizations');
|
||||
return {
|
||||
...originalModule,
|
||||
useDiscoverCustomizationService: () => mockCustomizationService,
|
||||
useDiscoverCustomizationService: () => ({
|
||||
customizationService: mockCustomizationService,
|
||||
isInitialized: Boolean(mockCustomizationService),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -64,10 +64,11 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
services,
|
||||
})
|
||||
);
|
||||
const customizationService = useDiscoverCustomizationService({
|
||||
customizationCallbacks,
|
||||
stateContainer,
|
||||
});
|
||||
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
|
||||
useDiscoverCustomizationService({
|
||||
customizationCallbacks,
|
||||
stateContainer,
|
||||
});
|
||||
const [error, setError] = useState<Error>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hasESData, setHasESData] = useState(false);
|
||||
|
@ -219,8 +220,9 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
[loadSavedSearch]
|
||||
);
|
||||
|
||||
// primary fetch: on initial search + triggered when id changes
|
||||
useEffect(() => {
|
||||
if (!isCustomizationServiceInitialized) return;
|
||||
|
||||
setLoading(true);
|
||||
setHasESData(false);
|
||||
setHasUserDataView(false);
|
||||
|
@ -228,16 +230,7 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
setError(undefined);
|
||||
// restore the previously selected data view for a new state
|
||||
loadSavedSearch(!savedSearchId ? stateContainer.internalState.getState().dataView : undefined);
|
||||
}, [
|
||||
loadSavedSearch,
|
||||
savedSearchId,
|
||||
stateContainer,
|
||||
setLoading,
|
||||
setHasESData,
|
||||
setHasUserDataView,
|
||||
setShowNoDataPage,
|
||||
setError,
|
||||
]);
|
||||
}, [isCustomizationServiceInitialized, loadSavedSearch, savedSearchId, stateContainer]);
|
||||
|
||||
// secondary fetch: in case URL is set to `/`, used to reset to 'new' state, keeping the current data view
|
||||
useUrl({
|
||||
|
|
|
@ -678,7 +678,7 @@ describe('Test discover state actions', () => {
|
|||
const { state } = await getState('/', { savedSearch: savedSearchMock });
|
||||
await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id });
|
||||
const unsubscribe = state.actions.initializeAndSync();
|
||||
await state.actions.onCreateDefaultAdHocDataView({ title: 'ad-hoc-test' });
|
||||
await state.actions.createAndAppendAdHocDataView({ title: 'ad-hoc-test' });
|
||||
expect(state.appState.getState().index).toBe('ad-hoc-id');
|
||||
expect(state.internalState.getState().adHocDataViews[0].id).toBe('ad-hoc-id');
|
||||
unsubscribe();
|
||||
|
|
|
@ -129,7 +129,7 @@ export interface DiscoverStateContainer {
|
|||
* Used by the Data View Picker
|
||||
* @param pattern
|
||||
*/
|
||||
onCreateDefaultAdHocDataView: (dataViewSpec: DataViewSpec) => Promise<void>;
|
||||
createAndAppendAdHocDataView: (dataViewSpec: DataViewSpec) => Promise<DataView>;
|
||||
/**
|
||||
* Triggered when a new data view is created
|
||||
* @param dataView
|
||||
|
@ -389,7 +389,7 @@ export function getDiscoverStateContainer({
|
|||
};
|
||||
};
|
||||
|
||||
const onCreateDefaultAdHocDataView = async (dataViewSpec: DataViewSpec) => {
|
||||
const createAndAppendAdHocDataView = async (dataViewSpec: DataViewSpec) => {
|
||||
const newDataView = await services.dataViews.create(dataViewSpec);
|
||||
if (newDataView.fields.getByName('@timestamp')?.type === 'date') {
|
||||
newDataView.timeFieldName = '@timestamp';
|
||||
|
@ -397,6 +397,7 @@ export function getDiscoverStateContainer({
|
|||
internalStateContainer.transitions.appendAdHocDataViews(newDataView);
|
||||
|
||||
await onChangeDataView(newDataView);
|
||||
return newDataView;
|
||||
};
|
||||
/**
|
||||
* Triggered when a user submits a query in the search bar
|
||||
|
@ -457,7 +458,7 @@ export function getDiscoverStateContainer({
|
|||
loadDataViewList,
|
||||
loadSavedSearch,
|
||||
onChangeDataView,
|
||||
onCreateDefaultAdHocDataView,
|
||||
createAndAppendAdHocDataView,
|
||||
onDataViewCreated,
|
||||
onDataViewEdited,
|
||||
onOpenSavedSearch,
|
||||
|
|
|
@ -39,15 +39,17 @@ describe('useDiscoverCustomizationService', () => {
|
|||
customizationCallbacks: [callback],
|
||||
})
|
||||
);
|
||||
expect(wrapper.result.current).toBeUndefined();
|
||||
expect(wrapper.result.current.isInitialized).toBe(false);
|
||||
expect(wrapper.result.current.customizationService).toBeUndefined();
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
const cleanup = jest.fn();
|
||||
await act(async () => {
|
||||
resolveCallback(cleanup);
|
||||
await promise;
|
||||
});
|
||||
expect(wrapper.result.current).toBeDefined();
|
||||
expect(wrapper.result.current).toBe(service);
|
||||
expect(wrapper.result.current.isInitialized).toBe(true);
|
||||
expect(wrapper.result.current.customizationService).toBeDefined();
|
||||
expect(wrapper.result.current.customizationService).toBe(service);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(cleanup).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
|
|
|
@ -49,7 +49,9 @@ export const useDiscoverCustomizationService = ({
|
|||
};
|
||||
});
|
||||
|
||||
return customizationService;
|
||||
const isInitialized = Boolean(customizationService);
|
||||
|
||||
return { customizationService, isInitialized };
|
||||
};
|
||||
|
||||
export const useDiscoverCustomization$ = <TCustomizationId extends DiscoverCustomizationId>(
|
||||
|
|
|
@ -30,10 +30,14 @@ export class Integration {
|
|||
}
|
||||
|
||||
public static create(integration: IntegrationType) {
|
||||
return new Integration({
|
||||
const integrationProps = {
|
||||
...integration,
|
||||
id: `integration-${integration.name}-${integration.version}` as IntegrationId,
|
||||
datasets: integration.dataStreams.map((dataset) => Dataset.create(dataset, integration)),
|
||||
title: integration.title ?? integration.name,
|
||||
};
|
||||
return new Integration({
|
||||
...integrationProps,
|
||||
datasets: integration.dataStreams.map((dataset) => Dataset.create(dataset, integrationProps)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ export const DatasetsPopover = ({
|
|||
iconSide="right"
|
||||
onClick={onClick}
|
||||
fullWidth={isMobile}
|
||||
data-test-subj={`${POPOVER_ID}-button`}
|
||||
>
|
||||
{iconType ? (
|
||||
<EuiIcon type={iconType} />
|
||||
|
|
|
@ -5,38 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import React from 'react';
|
||||
import { DatasetSelector } from '../components/dataset_selector';
|
||||
import { DatasetsProvider, useDatasetsContext } from '../hooks/use_datasets';
|
||||
import { InternalStateProvider } from '../hooks/use_data_view';
|
||||
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
||||
import { IDatasetsClient } from '../services/datasets';
|
||||
import {
|
||||
AllDatasetSelection,
|
||||
DatasetSelection,
|
||||
DatasetSelectionChange,
|
||||
} from '../utils/dataset_selection';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { useLogExplorerProfile } from '../hooks/use_log_explorer_profile';
|
||||
|
||||
interface CustomDatasetSelectorProps {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||
}
|
||||
|
||||
export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
|
||||
/**
|
||||
* TOREMOVE: This is a temporary workaround to control the datasetSelection value
|
||||
* until we handle the restore/initialization of the dataview with https://github.com/elastic/kibana/issues/160425,
|
||||
* where this value will be used to control the DatasetSelector selection with a top level state machine.
|
||||
*/
|
||||
const [datasetSelection, setDatasetSelection] = useState<DatasetSelection>(() =>
|
||||
AllDatasetSelection.create()
|
||||
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
||||
const { datasetSelection, handleDatasetSelectionChange } = useLogExplorerProfile(
|
||||
logExplorerProfileStateService
|
||||
);
|
||||
|
||||
// Restore All dataset selection on refresh until restore from url is not available
|
||||
React.useEffect(() => handleStreamSelection(datasetSelection), []);
|
||||
|
||||
const {
|
||||
error: integrationsError,
|
||||
integrations,
|
||||
|
@ -59,17 +44,6 @@ export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
|
|||
sortDatasets,
|
||||
} = useDatasetsContext();
|
||||
|
||||
/**
|
||||
* TODO: this action will be abstracted into a method of a class adapter in a follow-up PR
|
||||
* since we'll need to handle more actions from the stateContainer
|
||||
*/
|
||||
const handleStreamSelection: DatasetSelectionChange = (nextDatasetSelection) => {
|
||||
setDatasetSelection(nextDatasetSelection);
|
||||
return stateContainer.actions.onCreateDefaultAdHocDataView(
|
||||
nextDatasetSelection.toDataviewSpec()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DatasetSelector
|
||||
datasets={datasets}
|
||||
|
@ -85,7 +59,7 @@ export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
|
|||
onIntegrationsSort={sortIntegrations}
|
||||
onIntegrationsStreamsSearch={searchIntegrationsStreams}
|
||||
onIntegrationsStreamsSort={sortIntegrationsStreams}
|
||||
onSelectionChange={handleStreamSelection}
|
||||
onSelectionChange={handleDatasetSelectionChange}
|
||||
onStreamsEntryClick={loadDatasets}
|
||||
onUnmanagedStreamsReload={reloadDatasets}
|
||||
onUnmanagedStreamsSearch={searchDatasets}
|
||||
|
@ -103,17 +77,15 @@ export type CustomDatasetSelectorBuilderProps = CustomDatasetSelectorProps & {
|
|||
|
||||
function withProviders(Component: React.FunctionComponent<CustomDatasetSelectorProps>) {
|
||||
return function ComponentWithProviders({
|
||||
stateContainer,
|
||||
logExplorerProfileStateService,
|
||||
datasetsClient,
|
||||
}: CustomDatasetSelectorBuilderProps) {
|
||||
return (
|
||||
<InternalStateProvider value={stateContainer.internalState}>
|
||||
<IntegrationsProvider datasetsClient={datasetsClient}>
|
||||
<DatasetsProvider datasetsClient={datasetsClient}>
|
||||
<Component stateContainer={stateContainer} />
|
||||
</DatasetsProvider>
|
||||
</IntegrationsProvider>
|
||||
</InternalStateProvider>
|
||||
<IntegrationsProvider datasetsClient={datasetsClient}>
|
||||
<DatasetsProvider datasetsClient={datasetsClient}>
|
||||
<Component logExplorerProfileStateService={logExplorerProfileStateService} />
|
||||
</DatasetsProvider>
|
||||
</IntegrationsProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { CustomizationCallback } from '@kbn/discover-plugin/public';
|
||||
import React from 'react';
|
||||
import { dynamic } from '../utils/dynamic';
|
||||
|
@ -19,11 +19,29 @@ interface CreateLogExplorerProfileCustomizationsDeps {
|
|||
export const createLogExplorerProfileCustomizations =
|
||||
({ core }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||
async ({ customizations, stateContainer }) => {
|
||||
const { DatasetsService } = await import('../services/datasets');
|
||||
// Lazy load dependencies
|
||||
const datasetServiceModuleLoadable = import('../services/datasets');
|
||||
const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile');
|
||||
|
||||
const [{ DatasetsService }, { initializeLogExplorerProfileStateService, waitForState }] =
|
||||
await Promise.all([datasetServiceModuleLoadable, logExplorerMachineModuleLoadable]);
|
||||
|
||||
const datasetsService = new DatasetsService().start({
|
||||
http: core.http,
|
||||
});
|
||||
|
||||
const logExplorerProfileStateService = initializeLogExplorerProfileStateService({
|
||||
stateContainer,
|
||||
toasts: core.notifications.toasts,
|
||||
});
|
||||
|
||||
//
|
||||
/**
|
||||
* Wait for the machine to be fully initialized to set the restored selection
|
||||
* create the DataView and set it in the stateContainer from Discover
|
||||
*/
|
||||
await waitForState(logExplorerProfileStateService, 'initialized');
|
||||
|
||||
/**
|
||||
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
||||
*/
|
||||
|
@ -32,7 +50,7 @@ export const createLogExplorerProfileCustomizations =
|
|||
CustomDataViewPicker: () => (
|
||||
<LazyCustomDatasetSelector
|
||||
datasetsClient={datasetsService.client}
|
||||
stateContainer={stateContainer}
|
||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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 { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import { createStateContainerReactHelpers } from '@kbn/kibana-utils-plugin/common';
|
||||
|
||||
export const { Provider: InternalStateProvider, useSelector: useInternalStateSelector } =
|
||||
createStateContainerReactHelpers<DiscoverStateContainer['internalState']>();
|
||||
|
||||
export const useDataView = () => useInternalStateSelector((state) => state.dataView!);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { useSelector } from '@xstate/react';
|
||||
import { useCallback } from 'react';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { DatasetSelectionChange } from '../utils/dataset_selection';
|
||||
|
||||
export const useLogExplorerProfile = (
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService
|
||||
) => {
|
||||
const datasetSelection = useSelector(
|
||||
logExplorerProfileStateService,
|
||||
(state) => state.context.datasetSelection
|
||||
);
|
||||
|
||||
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
||||
(data) => {
|
||||
logExplorerProfileStateService.send({ type: 'UPDATE_DATASET_SELECTION', data });
|
||||
},
|
||||
[logExplorerProfileStateService]
|
||||
);
|
||||
|
||||
return { datasetSelection, handleDatasetSelectionChange };
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './src';
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import { InvokeCreator } from 'xstate';
|
||||
import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
||||
|
||||
interface LogExplorerProfileDataViewStateDependencies {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}
|
||||
|
||||
export const createAndSetDataView =
|
||||
({
|
||||
stateContainer,
|
||||
}: LogExplorerProfileDataViewStateDependencies): InvokeCreator<
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent
|
||||
> =>
|
||||
async (context) => {
|
||||
const dataView = await stateContainer.actions.createAndAppendAdHocDataView(
|
||||
context.datasetSelection.toDataviewSpec()
|
||||
);
|
||||
/**
|
||||
* We can't fully rely on the url update of the index param to create and restore the data view
|
||||
* due to a race condition where Discover, when initializing its internal logic,
|
||||
* check the value the index params before it gets updated in the line above.
|
||||
* In case the index param does not exist, it then create a internal saved search and set the current data view
|
||||
* to the existing one or the default logs-*.
|
||||
* We set explicitly the data view here to be used when restoring the data view on the initial load.
|
||||
*/
|
||||
stateContainer.actions.setDataView(dataView);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { AllDatasetSelection } from '../../../utils/dataset_selection';
|
||||
import { DefaultLogExplorerProfileState } from './types';
|
||||
|
||||
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
||||
datasetSelection: AllDatasetSelection.create(),
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './state_machine';
|
||||
export * from './types';
|
||||
export * from './utils';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { IToasts } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const createDatasetSelectionRestoreFailedNotifier = (toasts: IToasts) => () =>
|
||||
toasts.addWarning({
|
||||
title: i18n.translate(
|
||||
'xpack.discoverLogExplorer.datasetSelection.restoreDatasetSelectionFailedToastTitle',
|
||||
{ defaultMessage: "We couldn't restore your datasets selection." }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.discoverLogExplorer.datasetSelection.restoreDatasetSelectionFailedToastMessage',
|
||||
{ defaultMessage: 'We switched to "All log datasets" as the default selection.' }
|
||||
),
|
||||
});
|
||||
|
||||
export const createCreateDataViewFailedNotifier = (toasts: IToasts) => () =>
|
||||
toasts.addWarning({
|
||||
title: i18n.translate(
|
||||
'xpack.discoverLogExplorer.datasetSelection.createDataViewFailedToastTitle',
|
||||
{ defaultMessage: "We couldn't create a data view for your selection." }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.discoverLogExplorer.datasetSelection.createDataViewFailedToastMessage',
|
||||
{ defaultMessage: 'We switched to "All log datasets" as the default selection.' }
|
||||
),
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { IToasts } from '@kbn/core/public';
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import { actions, createMachine, interpret, InterpreterFrom } from 'xstate';
|
||||
import { isDatasetSelection } from '../../../utils/dataset_selection';
|
||||
import { createAndSetDataView } from './data_view_service';
|
||||
import { DEFAULT_CONTEXT } from './defaults';
|
||||
import {
|
||||
createCreateDataViewFailedNotifier,
|
||||
createDatasetSelectionRestoreFailedNotifier,
|
||||
} from './notifications';
|
||||
import type {
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent,
|
||||
LogExplorerProfileTypestate,
|
||||
} from './types';
|
||||
import { initializeFromUrl, listenUrlChange } 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>(
|
||||
{
|
||||
context: initialContext,
|
||||
predictableActionArguments: true,
|
||||
id: 'LogExplorerProfile',
|
||||
initial: 'uninitialized',
|
||||
states: {
|
||||
uninitialized: {
|
||||
always: 'initializingFromUrl',
|
||||
},
|
||||
initializingFromUrl: {
|
||||
invoke: {
|
||||
src: 'initializeFromUrl',
|
||||
onDone: {
|
||||
target: 'initializingDataView',
|
||||
actions: ['storeDatasetSelection'],
|
||||
},
|
||||
onError: {
|
||||
target: 'initializingDataView',
|
||||
actions: ['notifyDatasetSelectionRestoreFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
initializingDataView: {
|
||||
invoke: {
|
||||
src: 'createDataView',
|
||||
onDone: {
|
||||
target: 'initialized',
|
||||
},
|
||||
onError: {
|
||||
target: 'initialized',
|
||||
actions: ['notifyCreateDataViewFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
initialized: {
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
invoke: {
|
||||
src: 'listenUrlChange',
|
||||
},
|
||||
on: {
|
||||
UPDATE_DATASET_SELECTION: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['storeDatasetSelection'],
|
||||
},
|
||||
DATASET_SELECTION_RESTORE_FAILURE: {
|
||||
target: 'updatingDataView',
|
||||
actions: ['notifyDatasetSelectionRestoreFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
updatingDataView: {
|
||||
invoke: {
|
||||
src: 'createDataView',
|
||||
onDone: {
|
||||
target: 'idle',
|
||||
},
|
||||
onError: {
|
||||
target: 'idle',
|
||||
actions: ['notifyCreateDataViewFailed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
storeDatasetSelection: actions.assign((_context, event) =>
|
||||
'data' in event && isDatasetSelection(event.data)
|
||||
? {
|
||||
datasetSelection: event.data,
|
||||
}
|
||||
: {}
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface LogExplorerProfileStateMachineDependencies {
|
||||
initialContext?: LogExplorerProfileContext;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
toasts: IToasts;
|
||||
}
|
||||
|
||||
export const createLogExplorerProfileStateMachine = ({
|
||||
initialContext = DEFAULT_CONTEXT,
|
||||
stateContainer,
|
||||
toasts,
|
||||
}: LogExplorerProfileStateMachineDependencies) =>
|
||||
createPureLogExplorerProfileStateMachine(initialContext).withConfig({
|
||||
actions: {
|
||||
notifyCreateDataViewFailed: createCreateDataViewFailedNotifier(toasts),
|
||||
notifyDatasetSelectionRestoreFailed: createDatasetSelectionRestoreFailedNotifier(toasts),
|
||||
},
|
||||
services: {
|
||||
createDataView: createAndSetDataView({ stateContainer }),
|
||||
initializeFromUrl: initializeFromUrl({ stateContainer }),
|
||||
listenUrlChange: listenUrlChange({ stateContainer }),
|
||||
},
|
||||
});
|
||||
|
||||
export const initializeLogExplorerProfileStateService = (
|
||||
deps: LogExplorerProfileStateMachineDependencies
|
||||
) => {
|
||||
const machine = createLogExplorerProfileStateMachine(deps);
|
||||
|
||||
return interpret(machine).start();
|
||||
};
|
||||
|
||||
export type LogExplorerProfileStateService = InterpreterFrom<
|
||||
typeof createLogExplorerProfileStateMachine
|
||||
>;
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { DoneInvokeEvent } from 'xstate';
|
||||
import type { DatasetEncodingError, DatasetSelection } from '../../../utils/dataset_selection';
|
||||
|
||||
export interface WithDatasetSelection {
|
||||
datasetSelection: DatasetSelection;
|
||||
}
|
||||
|
||||
export type DefaultLogExplorerProfileState = WithDatasetSelection;
|
||||
|
||||
export type LogExplorerProfileTypestate =
|
||||
| {
|
||||
value: 'uninitialized';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initializingFromUrl';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initializingDataView';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initialized';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initialized.idle';
|
||||
context: WithDatasetSelection;
|
||||
}
|
||||
| {
|
||||
value: 'initialized.updatingDataView';
|
||||
context: WithDatasetSelection;
|
||||
};
|
||||
|
||||
export type LogExplorerProfileContext = LogExplorerProfileTypestate['context'];
|
||||
|
||||
export type LogExplorerProfileStateValue = LogExplorerProfileTypestate['value'];
|
||||
|
||||
export type LogExplorerProfileEvent =
|
||||
| {
|
||||
type: 'UPDATE_DATASET_SELECTION';
|
||||
data: DatasetSelection;
|
||||
}
|
||||
| {
|
||||
type: 'DATASET_SELECTION_RESTORE_FAILURE';
|
||||
}
|
||||
| DoneInvokeEvent<DatasetEncodingError>
|
||||
| DoneInvokeEvent<Error>;
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { InvokeCreator } from 'xstate';
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import {
|
||||
AllDatasetSelection,
|
||||
decodeDatasetSelectionId,
|
||||
hydrateDatasetSelection,
|
||||
isDatasetSelection,
|
||||
} from '../../../utils/dataset_selection';
|
||||
import type { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
||||
|
||||
interface LogExplorerProfileUrlStateDependencies {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}
|
||||
|
||||
export const listenUrlChange =
|
||||
({
|
||||
stateContainer,
|
||||
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent
|
||||
> =>
|
||||
(context) =>
|
||||
(send) => {
|
||||
const unsubscribe = stateContainer.appState.subscribe((nextState) => {
|
||||
const { index } = nextState;
|
||||
const prevIndex = stateContainer.appState.getPrevious().index;
|
||||
|
||||
// Preventing update if the index didn't change
|
||||
if (prevIndex === index) return;
|
||||
|
||||
try {
|
||||
const datasetSelection = extractDatasetSelectionFromIndex({ index, context });
|
||||
|
||||
if (isDatasetSelection(datasetSelection)) {
|
||||
send({ type: 'UPDATE_DATASET_SELECTION', data: datasetSelection });
|
||||
}
|
||||
} catch (error) {
|
||||
send({ type: 'DATASET_SELECTION_RESTORE_FAILURE' });
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
};
|
||||
|
||||
export const initializeFromUrl =
|
||||
({
|
||||
stateContainer,
|
||||
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent
|
||||
> =>
|
||||
async (context) => {
|
||||
const { index } = stateContainer.appState.getState();
|
||||
|
||||
return extractDatasetSelectionFromIndex({ index, context });
|
||||
};
|
||||
|
||||
const extractDatasetSelectionFromIndex = ({
|
||||
index,
|
||||
context,
|
||||
}: {
|
||||
index?: string;
|
||||
context: LogExplorerProfileContext;
|
||||
}) => {
|
||||
// If the index parameter doesn't exists, use initialContext value or fallback to AllDatasetSelection
|
||||
if (!index) {
|
||||
return context.datasetSelection ?? AllDatasetSelection.create();
|
||||
}
|
||||
|
||||
const rawDatasetSelection = decodeDatasetSelectionId(index);
|
||||
const datasetSelection = hydrateDatasetSelection(rawDatasetSelection);
|
||||
|
||||
return datasetSelection;
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { LogExplorerProfileStateService } from './state_machine';
|
||||
import { LogExplorerProfileStateValue } from './types';
|
||||
|
||||
export const waitForState = (
|
||||
service: LogExplorerProfileStateService,
|
||||
targetState: LogExplorerProfileStateValue
|
||||
) => {
|
||||
return new Promise((resolve) => {
|
||||
const { unsubscribe } = service.subscribe((state) => {
|
||||
if (state.matches(targetState)) {
|
||||
resolve(state);
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -11,8 +11,13 @@ import { SingleDatasetSelection } from './single_dataset_selection';
|
|||
export type DatasetSelection = AllDatasetSelection | SingleDatasetSelection;
|
||||
export type DatasetSelectionChange = (datasetSelection: DatasetSelection) => void;
|
||||
|
||||
export const isDatasetSelection = (input: any): input is DatasetSelection => {
|
||||
return input instanceof AllDatasetSelection || input instanceof SingleDatasetSelection;
|
||||
};
|
||||
|
||||
export * from './all_dataset_selection';
|
||||
export * from './single_dataset_selection';
|
||||
export * from './encoding';
|
||||
export * from './errors';
|
||||
export * from './hydrate_dataset_selection.ts';
|
||||
export * from './types';
|
||||
|
|
|
@ -13,6 +13,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
selectionType: 'single';
|
||||
selection: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
version?: string;
|
||||
dataset: Dataset;
|
||||
};
|
||||
|
@ -21,6 +22,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
this.selectionType = 'single';
|
||||
this.selection = {
|
||||
name: dataset.parentIntegration?.name,
|
||||
title: dataset.parentIntegration?.title,
|
||||
version: dataset.parentIntegration?.version,
|
||||
dataset,
|
||||
};
|
||||
|
@ -40,6 +42,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
selectionType: this.selectionType,
|
||||
selection: {
|
||||
name: this.selection.name,
|
||||
title: this.selection.title,
|
||||
version: this.selection.version,
|
||||
dataset: this.selection.dataset.toPlain(),
|
||||
},
|
||||
|
@ -47,10 +50,10 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
}
|
||||
|
||||
public static fromSelection(selection: SingleDatasetSelectionPayload) {
|
||||
const { name, version, dataset } = selection;
|
||||
const { name, title, version, dataset } = selection;
|
||||
|
||||
// Attempt reconstructing the integration object
|
||||
const integration = name && version ? { name, version } : undefined;
|
||||
const integration = name && version ? { name, title, version } : undefined;
|
||||
const datasetInstance = Dataset.create(dataset, integration);
|
||||
|
||||
return new SingleDatasetSelection(datasetInstance);
|
||||
|
|
|
@ -16,12 +16,17 @@ const integrationNameRT = rt.partial({
|
|||
name: rt.string,
|
||||
});
|
||||
|
||||
const integrationTitleRT = rt.partial({
|
||||
title: rt.string,
|
||||
});
|
||||
|
||||
const integrationVersionRT = rt.partial({
|
||||
version: rt.string,
|
||||
});
|
||||
|
||||
const singleDatasetSelectionPayloadRT = rt.intersection([
|
||||
integrationNameRT,
|
||||
integrationTitleRT,
|
||||
integrationVersionRT,
|
||||
rt.type({
|
||||
dataset: datasetRT,
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/fleet-plugin",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/io-ts-utils",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/rison",
|
||||
|
|
|
@ -8,22 +8,16 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'header', 'discover', 'timePicker', 'dashboard']);
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
'doc_table:legacy': false,
|
||||
};
|
||||
|
||||
describe('Customizations', () => {
|
||||
before('initialize tests', async () => {
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.uiSettings.update(defaultSettings);
|
||||
});
|
||||
|
||||
after('clean up archives', async () => {
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.uiSettings.unset('doc_table:legacy');
|
||||
});
|
||||
|
||||
describe('when Discover is loaded with the log-explorer profile', () => {
|
||||
|
@ -33,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.missingOrFail('dataset-selector-popover');
|
||||
|
||||
// Assert it renders on log-explorer profile
|
||||
await PageObjects.common.navigateToActualUrl('discover', 'p/log-explorer');
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
await testSubjects.existOrFail('dataset-selector-popover');
|
||||
});
|
||||
|
||||
|
@ -48,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.existOrFail('discoverSaveButton');
|
||||
|
||||
// Assert it renders on log-explorer profile
|
||||
await PageObjects.common.navigateToActualUrl('discover', 'p/log-explorer');
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
await testSubjects.missingOrFail('discoverNewButton');
|
||||
await testSubjects.missingOrFail('discoverOpenButton');
|
||||
await testSubjects.existOrFail('shareTopNavButton');
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
const PageObjects = getPageObjects(['common', 'discoverLogExplorer']);
|
||||
|
||||
describe('DatasetSelection initialization and update', () => {
|
||||
describe('when the "index" query param does not exist', () => {
|
||||
it('should initialize the "All log datasets" selection', async () => {
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
expect(datasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "index" query param exist', () => {
|
||||
it('should decode and restore the selection from a valid encoded index', async () => {
|
||||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
});
|
||||
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
expect(datasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
});
|
||||
|
||||
it('should fallback to "All log datasets" selection and notify the user for an invalid encoded index', async () => {
|
||||
const invalidEncodedIndex = 'invalid-encoded-index';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(invalidEncodedIndex)})`,
|
||||
});
|
||||
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
await PageObjects.discoverLogExplorer.assertRestoreFailureToastExist();
|
||||
expect(datasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating back and forth on the page history', () => {
|
||||
it('should decode and restore the selection for the current index', async () => {
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
const allDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(allDatasetSelectionTitle).to.be('All log datasets');
|
||||
|
||||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
});
|
||||
const azureDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(azureDatasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
|
||||
// Go back to previous page selection
|
||||
await retry.try(async () => {
|
||||
await browser.goBack();
|
||||
const backNavigationDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(backNavigationDatasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
|
||||
// Go forward to previous page selection
|
||||
await retry.try(async () => {
|
||||
await browser.goForward();
|
||||
const forwardNavigationDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(forwardNavigationDatasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Discover Log-Explorer profile', function () {
|
||||
loadTestFile(require.resolve('./customization'));
|
||||
loadTestFile(require.resolve('./dataset_selection_state'));
|
||||
});
|
||||
}
|
||||
|
|
31
x-pack/test/functional/page_objects/discover_log_explorer.ts
Normal file
31
x-pack/test/functional/page_objects/discover_log_explorer.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function DiscoverLogExplorerPageObject({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const toasts = getService('toasts');
|
||||
|
||||
return {
|
||||
async getDatasetSelectorButton() {
|
||||
return testSubjects.find('dataset-selector-popover-button');
|
||||
},
|
||||
|
||||
async getDatasetSelectorButtonText() {
|
||||
const button = await this.getDatasetSelectorButton();
|
||||
return button.getVisibleText();
|
||||
},
|
||||
|
||||
async assertRestoreFailureToastExist() {
|
||||
const successToast = await toasts.getToastElement(1);
|
||||
expect(await successToast.getVisibleText()).to.contain(
|
||||
"We couldn't restore your datasets selection"
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -7,87 +7,89 @@
|
|||
|
||||
import { pageObjects as kibanaFunctionalPageObjects } from '../../../../test/functional/page_objects';
|
||||
|
||||
import { AccountSettingsPageObject } from './account_settings_page';
|
||||
import { ApiKeysPageProvider } from './api_keys_page';
|
||||
import { BannersPageObject } from './banners_page';
|
||||
import { CanvasPageProvider } from './canvas_page';
|
||||
import { SecurityPageObject } from './security_page';
|
||||
import { MonitoringPageObject } from './monitoring_page';
|
||||
import { LogstashPageObject } from './logstash_page';
|
||||
import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page';
|
||||
import { CrossClusterReplicationPageProvider } from './cross_cluster_replication_page';
|
||||
import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections';
|
||||
import { DiscoverLogExplorerPageObject } from './discover_log_explorer';
|
||||
import { GeoFileUploadPageObject } from './geo_file_upload';
|
||||
import { GisPageObject } from './gis_page';
|
||||
import { GraphPageObject } from './graph_page';
|
||||
import { GrokDebuggerPageObject } from './grok_debugger_page';
|
||||
import { WatcherPageObject } from './watcher_page';
|
||||
import { ReportingPageObject } from './reporting_page';
|
||||
import { AccountSettingsPageObject } from './account_settings_page';
|
||||
import { ObservabilityPageProvider } from './observability_page';
|
||||
import { InfraHomePageProvider } from './infra_home_page';
|
||||
import { InfraLogsPageProvider } from './infra_logs_page';
|
||||
import { GisPageObject } from './gis_page';
|
||||
import { GeoFileUploadPageObject } from './geo_file_upload';
|
||||
import { StatusPageObject } from './status_page';
|
||||
import { UpgradeAssistantPageObject } from './upgrade_assistant_page';
|
||||
import { RollupPageObject } from './rollup_page';
|
||||
import { UptimePageObject } from './uptime_page';
|
||||
import { ApiKeysPageProvider } from './api_keys_page';
|
||||
import { LicenseManagementPageProvider } from './license_management_page';
|
||||
import { IndexManagementPageProvider } from './index_management_page';
|
||||
import { IndexLifecycleManagementPageProvider } from './index_lifecycle_management_page';
|
||||
import { SnapshotRestorePageProvider } from './snapshot_restore_page';
|
||||
import { CrossClusterReplicationPageProvider } from './cross_cluster_replication_page';
|
||||
import { RemoteClustersPageProvider } from './remote_clusters_page';
|
||||
import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page';
|
||||
import { LensPageProvider } from './lens_page';
|
||||
import { IndexManagementPageProvider } from './index_management_page';
|
||||
import { InfraHomePageProvider } from './infra_home_page';
|
||||
import { InfraHostsViewProvider } from './infra_hosts_view';
|
||||
import { InfraLogsPageProvider } from './infra_logs_page';
|
||||
import { InfraMetricsExplorerProvider } from './infra_metrics_explorer';
|
||||
import { InfraSavedViewsProvider } from './infra_saved_views';
|
||||
import { RoleMappingsPageProvider } from './role_mappings_page';
|
||||
import { SpaceSelectorPageObject } from './space_selector_page';
|
||||
import { IngestPipelinesPageProvider } from './ingest_pipelines_page';
|
||||
import { TagManagementPageObject } from './tag_management_page';
|
||||
import { NavigationalSearchPageObject } from './navigational_search';
|
||||
import { SearchSessionsPageProvider } from './search_sessions_management_page';
|
||||
import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections';
|
||||
import { BannersPageObject } from './banners_page';
|
||||
import { InfraHostsViewProvider } from './infra_hosts_view';
|
||||
import { LensPageProvider } from './lens_page';
|
||||
import { LicenseManagementPageProvider } from './license_management_page';
|
||||
import { LogstashPageObject } from './logstash_page';
|
||||
import { MaintenanceWindowsPageProvider } from './maintenance_windows_page';
|
||||
import { MonitoringPageObject } from './monitoring_page';
|
||||
import { NavigationalSearchPageObject } from './navigational_search';
|
||||
import { ObservabilityPageProvider } from './observability_page';
|
||||
import { RemoteClustersPageProvider } from './remote_clusters_page';
|
||||
import { ReportingPageObject } from './reporting_page';
|
||||
import { RoleMappingsPageProvider } from './role_mappings_page';
|
||||
import { RollupPageObject } from './rollup_page';
|
||||
import { SearchSessionsPageProvider } from './search_sessions_management_page';
|
||||
import { SecurityPageObject } from './security_page';
|
||||
import { SnapshotRestorePageProvider } from './snapshot_restore_page';
|
||||
import { SpaceSelectorPageObject } from './space_selector_page';
|
||||
import { StatusPageObject } from './status_page';
|
||||
import { TagManagementPageObject } from './tag_management_page';
|
||||
import { UpgradeAssistantPageObject } from './upgrade_assistant_page';
|
||||
import { UptimePageObject } from './uptime_page';
|
||||
import { WatcherPageObject } from './watcher_page';
|
||||
|
||||
// just like services, PageObjects are defined as a map of
|
||||
// names to Providers. Merge in Kibana's or pick specific ones
|
||||
export const pageObjects = {
|
||||
...kibanaFunctionalPageObjects,
|
||||
canvas: CanvasPageProvider,
|
||||
security: SecurityPageObject,
|
||||
accountSetting: AccountSettingsPageObject,
|
||||
monitoring: MonitoringPageObject,
|
||||
logstash: LogstashPageObject,
|
||||
apiKeys: ApiKeysPageProvider,
|
||||
banners: BannersPageObject,
|
||||
canvas: CanvasPageProvider,
|
||||
copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider,
|
||||
crossClusterReplication: CrossClusterReplicationPageProvider,
|
||||
detections: DetectionsPageObject,
|
||||
discoverLogExplorer: DiscoverLogExplorerPageObject,
|
||||
geoFileUpload: GeoFileUploadPageObject,
|
||||
graph: GraphPageObject,
|
||||
grokDebugger: GrokDebuggerPageObject,
|
||||
watcher: WatcherPageObject,
|
||||
reporting: ReportingPageObject,
|
||||
spaceSelector: SpaceSelectorPageObject,
|
||||
indexLifecycleManagement: IndexLifecycleManagementPageProvider,
|
||||
indexManagement: IndexManagementPageProvider,
|
||||
infraHome: InfraHomePageProvider,
|
||||
infraMetricsExplorer: InfraMetricsExplorerProvider,
|
||||
infraLogs: InfraLogsPageProvider,
|
||||
infraSavedViews: InfraSavedViewsProvider,
|
||||
infraHostsView: InfraHostsViewProvider,
|
||||
infraLogs: InfraLogsPageProvider,
|
||||
infraMetricsExplorer: InfraMetricsExplorerProvider,
|
||||
infraSavedViews: InfraSavedViewsProvider,
|
||||
ingestPipelines: IngestPipelinesPageProvider,
|
||||
lens: LensPageProvider,
|
||||
licenseManagement: LicenseManagementPageProvider,
|
||||
logstash: LogstashPageObject,
|
||||
maintenanceWindows: MaintenanceWindowsPageProvider,
|
||||
maps: GisPageObject,
|
||||
geoFileUpload: GeoFileUploadPageObject,
|
||||
monitoring: MonitoringPageObject,
|
||||
navigationalSearch: NavigationalSearchPageObject,
|
||||
observability: ObservabilityPageProvider,
|
||||
remoteClusters: RemoteClustersPageProvider,
|
||||
reporting: ReportingPageObject,
|
||||
roleMappings: RoleMappingsPageProvider,
|
||||
rollup: RollupPageObject,
|
||||
searchSessionsManagement: SearchSessionsPageProvider,
|
||||
security: SecurityPageObject,
|
||||
snapshotRestore: SnapshotRestorePageProvider,
|
||||
spaceSelector: SpaceSelectorPageObject,
|
||||
statusPage: StatusPageObject,
|
||||
tagManagement: TagManagementPageObject,
|
||||
upgradeAssistant: UpgradeAssistantPageObject,
|
||||
uptime: UptimePageObject,
|
||||
rollup: RollupPageObject,
|
||||
apiKeys: ApiKeysPageProvider,
|
||||
licenseManagement: LicenseManagementPageProvider,
|
||||
indexManagement: IndexManagementPageProvider,
|
||||
searchSessionsManagement: SearchSessionsPageProvider,
|
||||
indexLifecycleManagement: IndexLifecycleManagementPageProvider,
|
||||
tagManagement: TagManagementPageObject,
|
||||
snapshotRestore: SnapshotRestorePageProvider,
|
||||
crossClusterReplication: CrossClusterReplicationPageProvider,
|
||||
remoteClusters: RemoteClustersPageProvider,
|
||||
copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider,
|
||||
lens: LensPageProvider,
|
||||
roleMappings: RoleMappingsPageProvider,
|
||||
ingestPipelines: IngestPipelinesPageProvider,
|
||||
navigationalSearch: NavigationalSearchPageObject,
|
||||
banners: BannersPageObject,
|
||||
detections: DetectionsPageObject,
|
||||
observability: ObservabilityPageProvider,
|
||||
maintenanceWindows: MaintenanceWindowsPageProvider,
|
||||
watcher: WatcherPageObject,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('Customizations', () => {
|
||||
before('initialize tests', async () => {
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
});
|
||||
|
||||
after('clean up archives', async () => {
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
});
|
||||
|
||||
describe('when Discover is loaded with the log-explorer profile', () => {
|
||||
it('DatasetSelector should replace the DataViewPicker', async () => {
|
||||
// Assert does not render on discover app
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await testSubjects.missingOrFail('dataset-selector-popover');
|
||||
|
||||
// Assert it renders on log-explorer profile
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
await testSubjects.existOrFail('dataset-selector-popover');
|
||||
});
|
||||
|
||||
it('the TopNav bar should hide New, Open and Save options', async () => {
|
||||
// Assert does not render on discover app
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await testSubjects.existOrFail('discoverNewButton');
|
||||
await testSubjects.existOrFail('discoverOpenButton');
|
||||
await testSubjects.existOrFail('shareTopNavButton');
|
||||
await testSubjects.existOrFail('discoverAlertsButton');
|
||||
await testSubjects.existOrFail('openInspectorButton');
|
||||
await testSubjects.existOrFail('discoverSaveButton');
|
||||
|
||||
// Assert it renders on log-explorer profile
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
await testSubjects.missingOrFail('discoverNewButton');
|
||||
await testSubjects.missingOrFail('discoverOpenButton');
|
||||
await testSubjects.existOrFail('shareTopNavButton');
|
||||
await testSubjects.existOrFail('discoverAlertsButton');
|
||||
await testSubjects.existOrFail('openInspectorButton');
|
||||
await testSubjects.missingOrFail('discoverSaveButton');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
const PageObjects = getPageObjects(['common', 'discoverLogExplorer']);
|
||||
|
||||
describe('DatasetSelection initialization and update', () => {
|
||||
describe('when the "index" query param does not exist', () => {
|
||||
it('should initialize the "All log datasets" selection', async () => {
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
expect(datasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "index" query param exist', () => {
|
||||
it('should decode and restore the selection from a valid encoded index', async () => {
|
||||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
});
|
||||
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
expect(datasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
});
|
||||
|
||||
it('should fallback to "All log datasets" selection and notify the user for an invalid encoded index', async () => {
|
||||
const invalidEncodedIndex = 'invalid-encoded-index';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(invalidEncodedIndex)})`,
|
||||
});
|
||||
|
||||
const datasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
|
||||
await PageObjects.discoverLogExplorer.assertRestoreFailureToastExist();
|
||||
expect(datasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating back and forth on the page history', () => {
|
||||
it('should decode and restore the selection for the current index', async () => {
|
||||
await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' });
|
||||
const allDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(allDatasetSelectionTitle).to.be('All log datasets');
|
||||
|
||||
const azureActivitylogsIndex =
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA===';
|
||||
await PageObjects.common.navigateToApp('discover', {
|
||||
hash: `/p/log-explorer?_a=(index:${encodeURIComponent(azureActivitylogsIndex)})`,
|
||||
});
|
||||
const azureDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(azureDatasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
|
||||
// Go back to previous page selection
|
||||
await retry.try(async () => {
|
||||
await browser.goBack();
|
||||
const backNavigationDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(backNavigationDatasetSelectionTitle).to.be('All log datasets');
|
||||
});
|
||||
|
||||
// Go forward to previous page selection
|
||||
await retry.try(async () => {
|
||||
await browser.goForward();
|
||||
const forwardNavigationDatasetSelectionTitle =
|
||||
await PageObjects.discoverLogExplorer.getDatasetSelectorButtonText();
|
||||
expect(forwardNavigationDatasetSelectionTitle).to.be('[Azure Logs] activitylogs');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function (loadTestFile: FtrProviderContext['loadTestFile']) {
|
||||
describe('Discover Log-Explorer profile', function () {
|
||||
loadTestFile(require.resolve('./customization'));
|
||||
loadTestFile(require.resolve('./dataset_selection_state'));
|
||||
});
|
||||
}
|
|
@ -6,10 +6,12 @@
|
|||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import loadDiscoverLogExplorerSuite from './discover_log_explorer';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('serverless observability UI', function () {
|
||||
loadTestFile(require.resolve('./landing_page'));
|
||||
loadTestFile(require.resolve('./navigation'));
|
||||
loadDiscoverLogExplorerSuite(loadTestFile);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue