Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,32 @@ import { TopValuesPopover } from '../components/top_values_popover/top_values_po
import { AssistantOverlay } from '../../assistant/overlay';
import { useInitSourcerer } from '../../sourcerer/containers/use_init_sourcerer';
import { useInitDataViewManager } from '../../data_view_manager/hooks/use_init_data_view_manager';
import { useRestoreDataViewManagerStateFromURL } from '../../data_view_manager/hooks/use_sync_url_state';
import { useBrowserFields } from '../../data_view_manager/hooks/use_browser_fields';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { type BrowserFields } from '../../common/containers/source';

interface HomePageProps {
children: React.ReactNode;
}

const HomePageComponent: React.FC<HomePageProps> = ({ children }) => {
const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');

const { pathname } = useLocation();
const { browserFields } = useInitSourcerer(getScopeFromPath(pathname));
const sourcererScope = getScopeFromPath(pathname);
const { browserFields: oldBrowserFields } = useInitSourcerer(sourcererScope);
const { browserFields: experimentalBrowserFields } = useBrowserFields(sourcererScope);

useRestoreDataViewManagerStateFromURL(useInitDataViewManager(), sourcererScope);

useUrlState();
useUpdateBrowserTitle();
useUpdateExecutionContext();
useInitDataViewManager();

const browserFields = (
newDataViewPickerEnabled ? experimentalBrowserFields : oldBrowserFields
) as BrowserFields;

// side effect: this will attempt to upgrade the endpoint package if it is not up to date
// this will run when a user navigates to the Security Solution app and when they navigate between
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 const URL_PARAM_KEY = {
appQuery: 'query',
/** @deprecated */
eventFlyout: 'eventFlyout', // TODO remove when we assume it's been long enough that all users should use the newer `flyout` key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PhilippeOberti,do we have telemetry?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm I know we added telemetry when users open the flyout, but I don't think it logs what's in the url. Can't we just retrieve url information from Kibana telemetry? We should be able to query and see if any of the url we log contain the eventFlyout param?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR I think but I can remove it if you want.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, unrelated, but didn't want this to stay in forever. No need to remove here, just wanted to call attention to it so we could remove it later

flyout: 'flyout',
timelineFlyout: 'timelineFlyout',
filters: 'filters',
savedQuery: 'savedQuery',
sourcerer: 'sourcerer',
timeline: 'timeline',
timerange: 'timerange',
pageFilter: 'pageFilters',
rulesTable: 'rulesTable',
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const useInvestigateInTimeline = () => {
if (!keepDataView) {
if (newDataViewPickerEnabled) {
setSelectedDataView({
scope: [SourcererScopeName.timeline],
scope: SourcererScopeName.timeline,
id: defaultDataView.id,
fallbackPatterns: [signalIndexName || ''],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,4 @@ export const useUrlState = () => {
useQueryTimelineByIdOnUrlChange();
};

export const URL_PARAM_KEY = {
appQuery: 'query',
/** @deprecated */
eventFlyout: 'eventFlyout', // TODO remove when we assume it's been long enough that all users should use the newer `flyout` key
flyout: 'flyout',
timelineFlyout: 'timelineFlyout',
filters: 'filters',
savedQuery: 'savedQuery',
sourcerer: 'sourcerer',
timeline: 'timeline',
timerange: 'timerange',
pageFilter: 'pageFilters',
rulesTable: 'rulesTable',
} as const;
export { URL_PARAM_KEY } from './constants';
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import { TestProviders } from '../../../common/mock/test_providers';
import { useSelectDataView } from '../../hooks/use_select_data_view';
import { useUpdateUrlParam } from '../../../common/utils/global_query_string';
import { URL_PARAM_KEY } from '../../../common/hooks/constants';

jest.mock('../../../common/utils/global_query_string', () => ({
useUpdateUrlParam: jest.fn(),
}));

jest.mock('../../hooks/use_data_view_spec', () => ({
useDataViewSpec: jest.fn(),
Expand Down Expand Up @@ -66,6 +72,8 @@ describe('DataViewPicker', () => {
let mockDispatch = jest.fn();

beforeEach(() => {
jest.mocked(useUpdateUrlParam).mockReturnValue(jest.fn());

jest.mocked(useDataViewSpec).mockReturnValue({
dataViewSpec: {
id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID,
Expand Down Expand Up @@ -113,7 +121,24 @@ describe('DataViewPicker', () => {

expect(jest.mocked(useSelectDataView())).toHaveBeenCalledWith({
id: 'new-data-view-id',
scope: ['default'],
scope: 'default',
});
});

it('calls useUpdateUrlParam when changing the data view', () => {
render(
<TestProviders>
<DataViewPicker scope={DataViewManagerScopeName.default} />
</TestProviders>
);

fireEvent.click(screen.getByTestId('changeDataView'));

expect(jest.mocked(useUpdateUrlParam(URL_PARAM_KEY.sourcerer))).toHaveBeenCalledWith({
default: {
id: 'new-data-view-id',
selectedPatterns: [],
},
});
});

Expand Down Expand Up @@ -144,7 +169,7 @@ describe('DataViewPicker', () => {
);
expect(jest.mocked(useSelectDataView())).toHaveBeenCalledWith({
id: 'new-data-view-id',
scope: ['default'],
scope: 'default',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ import React, { useCallback, useRef, useMemo, memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { DataView } from '@kbn/data-views-plugin/public';
import type { DataViewManagerScopeName } from '../../constants';
import type { SourcererUrlState } from '../../../sourcerer/store/model';
import { useUpdateUrlParam } from '../../../common/utils/global_query_string';
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { useKibana } from '../../../common/lib/kibana';
import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants';
import { useDataViewSpec } from '../../hooks/use_data_view_spec';
import { sharedStateSelector } from '../../redux/selectors';
import { sharedDataViewManagerSlice } from '../../redux/slices';
import { useSelectDataView } from '../../hooks/use_select_data_view';
import { DATA_VIEW_PICKER_TEST_ID } from './constants';
import { DataViewManagerScopeName, DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../../constants';
import { useManagedDataViews } from '../../hooks/use_managed_data_views';
import { useSavedDataViews } from '../../hooks/use_saved_data_views';
import { DEFAULT_SECURITY_DATA_VIEW, LOADING } from './translations';
import { DATA_VIEW_PICKER_TEST_ID } from './constants';

interface DataViewPickerProps {
/**
Expand Down Expand Up @@ -49,24 +51,43 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie

const { dataViewSpec, status } = useDataViewSpec(scope);

const isDefaultSourcerer = scope === DataViewManagerScopeName.default;
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(URL_PARAM_KEY.sourcerer);

const dataViewId = dataViewSpec?.id;

// NOTE: this function is called in response to user interaction with the picker,
// hence - it is the only place where we should update the url param for the data view selection.
const handleChangeDataView = useCallback(
(id: string, indexPattern: string = '') => {
selectDataView({ id, scope });

if (isDefaultSourcerer) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but defaultSourcerer is vague, but I'm not sure what to rename it to or how to classify the other scopes of timeline,analyzer, and the detections page 🤔 . Anyways, just wanted to bring it up for discussion separately

updateUrlParam({
[DataViewManagerScopeName.default]: {
id,
// NOTE: Boolean filter for removing empty patterns
selectedPatterns: indexPattern.split(',').filter(Boolean),
},
});
}
},
[isDefaultSourcerer, scope, selectDataView, updateUrlParam]
);

const createNewDataView = useCallback(() => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (newDataView) => {
if (!newDataView.id) {
return;
}

dispatch(sharedDataViewManagerSlice.actions.addDataView(newDataView));
selectDataView({ id: newDataView.id, scope: [scope] });
handleChangeDataView(newDataView.id, newDataView.getIndexPattern());
},
allowAdHocDataView: true,
});
}, [dataViewEditor, dispatch, scope, selectDataView]);

const handleChangeDataView = useCallback(
(id: string) => {
selectDataView({ id, scope: [scope] });
},
[scope, selectDataView]
);
}, [dataViewEditor, dispatch, handleChangeDataView]);

const editField = useCallback(
async (fieldName?: string, _uiAction: 'edit' | 'add' = 'edit') => {
Expand All @@ -86,7 +107,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie
return;
}

handleChangeDataView(dataViewInstance.id);
handleChangeDataView(dataViewInstance.id, dataViewInstance.getIndexPattern());
},
});
},
Expand All @@ -98,9 +119,12 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie
*/
const handleDataViewModified = useCallback(
(updatedDataView: DataView) => {
selectDataView({ id: updatedDataView.id, scope: [scope] });
if (!updatedDataView.id) {
return;
}
handleChangeDataView(updatedDataView.id, updatedDataView.getIndexPattern());
},
[scope, selectDataView]
[handleChangeDataView]
);

const handleAddField = useCallback(() => editField(undefined, 'add'), [editField]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ describe('useInitDataViewPicker', () => {
it('should render and dispatch an init action', () => {
renderHook(
() => {
return useInitDataViewManager();
return useInitDataViewManager()([]);
},
{ wrapper: TestProviders }
);

expect(useDispatch()).toHaveBeenCalledWith(sharedDataViewManagerSlice.actions.init());
expect(useDispatch()).toHaveBeenCalledWith(sharedDataViewManagerSlice.actions.init([]));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import type { AnyAction, Dispatch, ListenerEffectAPI } from '@reduxjs/toolkit';
import {
addListener as originalAddListener,
Expand All @@ -18,6 +18,8 @@ import { createDataViewSelectedListener } from '../redux/listeners/data_view_sel
import { createInitListener } from '../redux/listeners/init_listener';
import { useEnableExperimental } from '../../common/hooks/use_experimental_features';
import { sharedDataViewManagerSlice } from '../redux/slices';
import { type SelectDataViewAsyncPayload } from '../redux/actions';
import { DataViewManagerScopeName } from '../constants';

type OriginalListener = Parameters<typeof originalAddListener>[0];

Expand Down Expand Up @@ -49,19 +51,39 @@ export const useInitDataViewManager = () => {
dataViews: services.dataViews,
});

const dataViewSelectedListener = createDataViewSelectedListener({
dataViews: services.dataViews,
});

dispatch(addListener(dataViewsLoadingListener));
dispatch(addListener(dataViewSelectedListener));

// NOTE: Every scope has its own listener instance; this allows for cancellation
const listeners = [
DataViewManagerScopeName.default,
DataViewManagerScopeName.timeline,
DataViewManagerScopeName.detections,
DataViewManagerScopeName.analyzer,
].map((scope) =>
createDataViewSelectedListener({
scope,
dataViews: services.dataViews,
})
);

listeners.forEach((dataViewSelectedListener) => {
dispatch(addListener(dataViewSelectedListener));
});

// NOTE: this kicks off the data loading in the Data View Picker
dispatch(sharedDataViewManagerSlice.actions.init());

return () => {
dispatch(removeListener(dataViewsLoadingListener));
dispatch(removeListener(dataViewSelectedListener));
listeners.forEach((dataViewSelectedListener) => {
dispatch(removeListener(dataViewSelectedListener));
});
};
}, [dispatch, newDataViewPickerEnabled, services.dataViews]);

return useCallback(
(initialSelection: SelectDataViewAsyncPayload[]) => {
dispatch(sharedDataViewManagerSlice.actions.init(initialSelection));
},
[dispatch]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ describe('useSelectDataView', () => {
{ wrapper: TestProviders }
);

result.current({ id: 'test', scope: [DataViewManagerScopeName.default] });
result.current({ id: 'test', scope: DataViewManagerScopeName.default });

expect(useDispatch()).toHaveBeenCalledWith({
payload: { id: 'test', scope: ['default'] },
payload: { id: 'test', scope: 'default' },
type: 'x-pack/security_solution/dataViewManager/selectDataView',
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { useCallback } from 'react';
import type { DataViewManagerScopeName } from '../constants';
import { selectDataViewAsync } from '../redux/actions';

/**
* This hook wraps the dispatch call that updates the redux store with new data view selection.
Comment thread
lgestc marked this conversation as resolved.
* It is the recommended entry point for altering the data view selection.
* Manual action dispatches are not required and should be avoided outside of the data view manager scope.
*/
export const useSelectDataView = () => {
const dispatch = useDispatch();

Expand All @@ -24,7 +29,7 @@ export const useSelectDataView = () => {
/**
* Data view selection will be applied to the scopes listed here
*/
scope: DataViewManagerScopeName[];
scope: DataViewManagerScopeName;
}) => {
dispatch(selectDataViewAsync(params));
},
Expand Down
Loading