[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:
Marco Antonio Ghiani 2023-07-20 09:21:08 +02:00 committed by GitHub
parent a3e1aab363
commit 446157f6d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 861 additions and 158 deletions

View file

@ -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,

View file

@ -27,7 +27,10 @@ jest.mock('../../customizations', () => {
const originalModule = jest.requireActual('../../customizations');
return {
...originalModule,
useDiscoverCustomizationService: () => mockCustomizationService,
useDiscoverCustomizationService: () => ({
customizationService: mockCustomizationService,
isInitialized: Boolean(mockCustomizationService),
}),
};
});

View file

@ -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({

View file

@ -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();

View file

@ -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,

View file

@ -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();

View file

@ -49,7 +49,9 @@ export const useDiscoverCustomizationService = ({
};
});
return customizationService;
const isInitialized = Boolean(customizationService);
return { customizationService, isInitialized };
};
export const useDiscoverCustomization$ = <TCustomizationId extends DiscoverCustomizationId>(

View file

@ -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)),
});
}
}

View file

@ -53,6 +53,7 @@ export const DatasetsPopover = ({
iconSide="right"
onClick={onClick}
fullWidth={isMobile}
data-test-subj={`${POPOVER_ID}-button`}
>
{iconType ? (
<EuiIcon type={iconType} />

View file

@ -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>
);
};
}

View file

@ -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}
/>
),
});

View file

@ -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!);

View file

@ -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 };
};

View file

@ -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';

View file

@ -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);
};

View file

@ -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(),
};

View file

@ -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';

View file

@ -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.' }
),
});

View file

@ -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
>;

View file

@ -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>;

View file

@ -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;
};

View file

@ -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();
}
});
});
};

View file

@ -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';

View file

@ -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);

View file

@ -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,

View file

@ -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",

View file

@ -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');

View file

@ -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');
});
});
});
});
}

View file

@ -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'));
});
}

View 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"
);
},
};
}

View file

@ -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,
};

View file

@ -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');
});
});
});
}

View file

@ -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');
});
});
});
});
}

View file

@ -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'));
});
}

View file

@ -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);
});
}