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
@@ -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 React from 'react';
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';

export const PageLoader = () => (
<EuiEmptyPrompt icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />} />
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
* 2.0.
*/

import { act, renderHook } from '@testing-library/react';
import { TestProviders } from '../../common/mock';
import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../constants';
import { act, renderHook, waitFor } from '@testing-library/react';
import { DataView } from '@kbn/data-views-plugin/public';

import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, DataViewManagerScopeName } from '../constants';
import { useDataView } from './use_data_view';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { useSelector } from 'react-redux';
import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';

jest.mock('../../common/hooks/use_experimental_features');

Expand All @@ -20,32 +21,148 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

const mockGet = jest.fn();
const mockToastsDanger = jest.fn();

const mockNotifications = {
toasts: {
danger: mockToastsDanger,
},
};

const mockDataViews = { get: mockGet };

const fakeDataView = new DataView({
spec: {
id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID,
},
fieldFormats: {} as FieldFormatsStartCommon,
});

jest.mock('../../common/lib/kibana', () => {
const actual = jest.requireActual('../../common/lib/kibana');
return {
...actual,
useKibana: () => ({
services: {
dataViews: mockDataViews,
},
notifications: mockNotifications,
}),
};
});

describe('useDataView', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true);
jest
.mocked(useSelector)
.mockReturnValue({ dataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, status: 'ready' });
});

describe('when data view is available', () => {
it('should return DataView instance', async () => {
const wrapper = renderHook(() => useDataView(DataViewManagerScopeName.default), {
wrapper: TestProviders,
});
it('should return DataView instance when data view is available', async () => {
mockGet.mockResolvedValue(fakeDataView);

const { result } = renderHook(() => useDataView());

expect(result.current.dataView).not.toBe(undefined);
expect(result.current.dataView.id).toBe(undefined);
expect(result.current.status).toBe('pristine');

// NOTE: should switch to ready almost immediately
await waitFor(() => {
expect(result.current.status).toEqual('ready');
});

expect(result.current.dataView.id).toBe(DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID);
});

it('should set status to loading on subsequent calls after first load', async () => {
mockGet.mockResolvedValue(fakeDataView);

const { result, rerender } = renderHook(() => useDataView());

// First load, no loading state
expect(result.current.status).toBe('pristine');
await waitFor(() => {
if (result.current.status === 'loading') {
// if loading is returned here, we have an error. until the first data view is loaded from the service, we want to stay "pristine".
// this is because there are elements on some pages that depending on this behavior.
return;
}

expect(result.current.status).toEqual('ready');
});

jest
.mocked(useSelector)
.mockReturnValue({ dataViewId: 'different-data-view', status: 'ready' });

// Dont await on purpose
act(() => rerender());

// Should be loading at some point
await waitFor(() => {
expect(result.current.status).toEqual('loading');
});
});

it('should not call get if newDataViewPickerEnabled is false', async () => {
jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(false);

const { result, rerender } = renderHook(() => useDataView(DataViewManagerScopeName.default));

await act(async () => rerender(DataViewManagerScopeName.default));
expect(mockGet).not.toHaveBeenCalled();
expect(result.current.status).toBe('pristine');
});

it('should not call get if dataViewId is missing', async () => {
jest.mocked(useSelector).mockReturnValue({ dataViewId: undefined, status: 'ready' });

const { result, rerender } = renderHook(() => useDataView(DataViewManagerScopeName.default));

await act(async () => rerender(DataViewManagerScopeName.default));
expect(mockGet).not.toHaveBeenCalled();
expect(result.current.status).toBe('pristine');
});

it('should not call get if status is not ready', async () => {
jest
.mocked(useSelector)
.mockReturnValue({ dataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, status: 'loading' });

const { result, rerender } = renderHook(() => useDataView(DataViewManagerScopeName.default));

await act(async () => wrapper.rerender(DataViewManagerScopeName.default));
expect(wrapper.result.current.dataView).toBeTruthy();
await act(async () => rerender(DataViewManagerScopeName.default));
expect(mockGet).not.toHaveBeenCalled();
expect(result.current.status).toBe('pristine');
});

it('should set status to error and call toasts.danger on get error', async () => {
mockGet.mockRejectedValue(new Error('fail!'));

const { result, rerender } = renderHook(() => useDataView(DataViewManagerScopeName.default));

await act(async () => rerender(DataViewManagerScopeName.default));
expect(result.current.status).toBe('error');
expect(mockToastsDanger).toHaveBeenCalledWith({
title: 'Error retrieving data view',
body: expect.stringContaining('fail!'),
});
});

describe('when data view fields are not available', () => {
it('should return undefined', () => {
const wrapper = renderHook(() => useDataView(DataViewManagerScopeName.default), {
wrapper: TestProviders,
});
it('should handle unknown error shape gracefully', async () => {
mockGet.mockRejectedValue({});

const { result, rerender } = renderHook(() => useDataView(DataViewManagerScopeName.default));

expect(wrapper.result.current.dataView).toBeUndefined();
await act(async () => rerender(DataViewManagerScopeName.default));
expect(result.current.status).toBe('error');
expect(mockToastsDanger).toHaveBeenCalledWith({
title: 'Error retrieving data view',
body: expect.stringContaining('unknown'),
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,28 @@
* 2.0.
*/

import { useEffect, useMemo, useState } from 'react';
import { type DataView } from '@kbn/data-views-plugin/public';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/public';

import { useSelector } from 'react-redux';
import { type FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
import { useKibana } from '../../common/lib/kibana';
import { DataViewManagerScopeName } from '../constants';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { sourcererAdapterSelector } from '../redux/selectors';
import type { SharedDataViewSelectionState } from '../redux/types';

const INITIAL_DV = new DataView({
fieldFormats: {} as FieldFormatsStartCommon,
});

/*
* This hook should be used whenever we need the actual DataView and not just the spec for the
* selected data view.
*/
export const useDataView = (
dataViewManagerScope: DataViewManagerScopeName = DataViewManagerScopeName.default
): { dataView: DataView | undefined; status: SharedDataViewSelectionState['status'] } => {
): { dataView: DataView; status: SharedDataViewSelectionState['status'] } => {
const {
services: { dataViews },
notifications,
Expand All @@ -31,36 +36,51 @@ export const useDataView = (
sourcererAdapterSelector(dataViewManagerScope)
);
const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');
const [retrievedDataView, setRetrievedDataView] = useState<DataView | undefined>();
const [localStatus, setLocalStatus] =
useState<SharedDataViewSelectionState['status']>('pristine');
const [retrievedDataView, setRetrievedDataView] = useState<DataView>(INITIAL_DV);
const loadedForTheFirstTimeRef = useRef(false);

useEffect(() => {
(async () => {
if (!newDataViewPickerEnabled) {
return;
}

if (!dataViewId || internalStatus !== 'ready') {
return setRetrievedDataView(undefined);
return;
}

if (loadedForTheFirstTimeRef.current) {
setLocalStatus('loading');
}

try {
// TODO: remove conditional .get call when new data view picker is stabilized
// this is due to the fact that many of our tests mock kibana hook and do not provide proper
// double for dataViews service
const currDv = await dataViews?.get(dataViewId);
if (!loadedForTheFirstTimeRef.current) {
loadedForTheFirstTimeRef.current = true;
}
setRetrievedDataView(currDv);
setLocalStatus('ready');
} catch (error) {
setRetrievedDataView(undefined);
// TODO: (remove conditional call when feature flag is on (mocks are broken for some tests))
notifications?.toasts?.danger({
title: 'Error retrieving data view',
body: `Error: ${error?.message ?? 'unknown'}`,
});
setLocalStatus('error');
}
})();
}, [dataViews, dataViewId, internalStatus, notifications]);
}, [dataViews, dataViewId, internalStatus, notifications, newDataViewPickerEnabled]);

return useMemo(() => {
if (!newDataViewPickerEnabled) {
return { dataView: undefined, status: internalStatus };
return { dataView: retrievedDataView, status: localStatus };
}

return { dataView: retrievedDataView, status: retrievedDataView ? internalStatus : 'loading' };
}, [newDataViewPickerEnabled, retrievedDataView, internalStatus]);
return { dataView: retrievedDataView, status: localStatus };
}, [newDataViewPickerEnabled, retrievedDataView, localStatus]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime
import { useDataViewSpec } from '../../data_view_manager/hooks/use_data_view_spec';
import { useDataView } from '../../data_view_manager/hooks/use_data_view';
import { useStoreEntityTypes } from '../hooks/use_enabled_entity_types';
import { PageLoader } from '../../common/components/page_loader';

const EntityAnalyticsComponent = () => {
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
Expand Down Expand Up @@ -60,6 +61,10 @@ const EntityAnalyticsComponent = () => {
const isEntityStoreFeatureFlagDisabled = useIsExperimentalFeatureEnabled('entityStoreDisabled');
const entityTypes = useStoreEntityTypes();

if (newDataViewPickerEnabled && status === 'pristine') {
return <PageLoader />;
}

return (
<>
{indicesExist ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { useDataView } from '../../../../data_view_manager/hooks/use_data_view';
import { useDataViewSpec } from '../../../../data_view_manager/hooks/use_data_view_spec';
import { useSelectedPatterns } from '../../../../data_view_manager/hooks/use_selected_patterns';
import { PageLoader } from '../../../../common/components/page_loader';

const ES_HOST_FIELD = 'host.name';
const HostOverviewManage = manageQuery(HostOverview);
Expand Down Expand Up @@ -142,7 +143,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta

const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');

const { dataView } = useDataView();
const { dataView, status } = useDataView();
const { dataViewSpec } = useDataViewSpec();
const experimentalSelectedPatterns = useSelectedPatterns();

Expand Down Expand Up @@ -225,6 +226,10 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
onChange: calculateEntityRiskScore,
});

if (newDataViewPickerEnabled && status === 'pristine') {
return <PageLoader />;
}

return (
<>
{indicesExist ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { useLicense } from '../../../common/hooks/use_license';
import { useDataView } from '../../../data_view_manager/hooks/use_data_view';
import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec';
import { useSelectedPatterns } from '../../../data_view_manager/hooks/use_selected_patterns';
import { PageLoader } from '../../../common/components/page_loader';

/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
Expand Down Expand Up @@ -117,7 +118,7 @@ const HostsComponent = () => {

const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');

const { dataView } = useDataView();
const { dataView, status } = useDataView();
const { dataViewSpec } = useDataViewSpec();
const experimentalSelectedPatterns = useSelectedPatterns();

Expand Down Expand Up @@ -185,6 +186,10 @@ const HostsComponent = () => {
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
);

if (newDataViewPickerEnabled && status === 'pristine') {
return <PageLoader />;
}

return (
<>
{indicesExist ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { FlowTargetSelectConnected } from '../../components/flow_target_select_c
import type { IpOverviewProps } from '../../components/details';
import { IpOverview } from '../../components/details';
import { SiemSearchBar } from '../../../../common/components/search_bar';
import { PageLoader } from '../../../../common/components/page_loader';
import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper';
import { useNetworkDetails, ID } from '../../containers/details';
import { useKibana } from '../../../../common/lib/kibana';
Expand Down Expand Up @@ -118,7 +119,7 @@ const NetworkDetailsComponent: React.FC = () => {

const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');

const { dataView } = useDataView();
const { dataView, status } = useDataView();
const { dataViewSpec } = useDataViewSpec();
const experimentalSelectedPatterns = useSelectedPatterns();

Expand Down Expand Up @@ -193,6 +194,10 @@ const NetworkDetailsComponent: React.FC = () => {
return dataViewSpecToViewBase(sourcererDataView);
}, [sourcererDataView]);

if (newDataViewPickerEnabled && status === 'pristine') {
return <PageLoader />;
}

return (
<div data-test-subj="network-details-page">
{indicesExist ? (
Expand Down
Loading