Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring network details to use useSearchStrategy #135995

Merged
merged 9 commits into from
Jul 21, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { act, renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../../../common/mock';
import { ID, useNetworkDetails } from '.';
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';

jest.mock('../../../common/containers/use_search_strategy', () => ({
useSearchStrategy: jest.fn(),
}));
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
const mockSearch = jest.fn();

const defaultProps = {
id: ID,
indexNames: ['fakebeat-*'],
ip: '192.168.1.1',
skip: false,
};

describe('useNetworkDetails', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseSearchStrategy.mockReturnValue({
loading: false,
result: {
networkDetails: {},
},
search: mockSearch,
refetch: jest.fn(),
inspect: {},
});
});

it('runs search', () => {
renderHook(() => useNetworkDetails(defaultProps), {
wrapper: TestProviders,
});

expect(mockSearch).toHaveBeenCalled();
});

it('does not run search when skip = true', () => {
const props = {
...defaultProps,
skip: true,
};
renderHook(() => useNetworkDetails(props), {
wrapper: TestProviders,
});

expect(mockSearch).not.toHaveBeenCalled();
});
it('skip = true will cancel any running request', () => {
const props = {
...defaultProps,
};
const { rerender } = renderHook(() => useNetworkDetails(props), {
wrapper: TestProviders,
});
props.skip = true;
act(() => rerender());
expect(mockUseSearchStrategy).toHaveBeenCalledTimes(2);
expect(mockUseSearchStrategy.mock.calls[1][0].abort).toEqual(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,17 @@
* 2.0.
*/

import { noop } from 'lodash/fp';
import { useState, useEffect, useCallback, useRef } from 'react';
import deepEqual from 'fast-deep-equal';
import { Subscription } from 'rxjs';
import { useEffect, useMemo } from 'react';

import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
import type { ESTermQuery } from '../../../../common/typed_json';
import type { inputsModel } from '../../../common/store';
import { useKibana } from '../../../common/lib/kibana';
import { createFilter } from '../../../common/containers/helpers';
import type {
NetworkDetailsRequestOptions,
NetworkDetailsStrategyResponse,
} from '../../../../common/search_strategy';
import type { NetworkDetailsStrategyResponse } from '../../../../common/search_strategy';
import { NetworkQueries } from '../../../../common/search_strategy';
import * as i18n from './translations';
import { getInspectResponse } from '../../../helpers';
import type { InspectResponse } from '../../../types';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';

import { useSearchStrategy } from '../../../common/containers/use_search_strategy';

export const ID = 'networkDetailsQuery';

Expand All @@ -36,111 +28,61 @@ export interface NetworkDetailsArgs {
}

interface UseNetworkDetails {
filterQuery?: ESTermQuery | string;
id?: string;
ip: string;
indexNames: string[];
filterQuery?: ESTermQuery | string;
ip: string;
skip: boolean;
}

export const useNetworkDetails = ({
filterQuery,
indexNames,
id = ID,
skip,
indexNames,
ip,
skip,
}: UseNetworkDetails): [boolean, NetworkDetailsArgs] => {
const { data } = useKibana().services;
const refetch = useRef<inputsModel.Refetch>(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const [loading, setLoading] = useState(false);

const [networkDetailsRequest, setNetworkDetailsRequest] =
useState<NetworkDetailsRequestOptions | null>(null);

const [networkDetailsResponse, setNetworkDetailsResponse] = useState<NetworkDetailsArgs>({
networkDetails: {},
id,
inspect: {
dsl: [],
response: [],
const {
loading,
result: response,
search,
refetch,
inspect,
} = useSearchStrategy<NetworkQueries.details>({
factoryQueryType: NetworkQueries.details,
initialResult: {
networkDetails: {},
},
isInspected: false,
refetch: refetch.current,
errorMessage: i18n.ERROR_NETWORK_DETAILS,
abort: skip,
});
const { addError, addWarning } = useAppToasts();

const networkDetailsSearch = useCallback(
(request: NetworkDetailsRequestOptions | null) => {
if (request == null || skip) {
return;
}
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<NetworkDetailsRequestOptions, NetworkDetailsStrategyResponse>(request, {
strategy: 'securitySolutionSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
setLoading(false);
setNetworkDetailsResponse((prevResponse) => ({
...prevResponse,
networkDetails: response.networkDetails,
inspect: getInspectResponse(response, prevResponse.inspect),
refetch: refetch.current,
}));
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading(false);
addWarning(i18n.ERROR_NETWORK_DETAILS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_NETWORK_DETAILS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, addError, addWarning, skip]
const networkDetailsResponse = useMemo(
() => ({
networkDetails: response.networkDetails,
id,
inspect,
isInspected: false,
refetch,
}),
[id, inspect, refetch, response.networkDetails]
);

useEffect(() => {
setNetworkDetailsRequest((prevRequest) => {
const myRequest = {
...(prevRequest ?? {}),
defaultIndex: indexNames,
factoryQueryType: NetworkQueries.details,
filterQuery: createFilter(filterQuery),
ip,
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [indexNames, filterQuery, ip, id]);
const networkDetailsRequest = useMemo(
() => ({
defaultIndex: indexNames,
factoryQueryType: NetworkQueries.details,
filterQuery: createFilter(filterQuery),
ip,
}),
[filterQuery, indexNames, ip]
);

useEffect(() => {
networkDetailsSearch(networkDetailsRequest);
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [networkDetailsRequest, networkDetailsSearch]);
if (!skip && networkDetailsRequest) {
search(networkDetailsRequest);
}
}, [networkDetailsRequest, search, skip]);

return [loading, networkDetailsResponse];
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import type { TimelineExpandedDetail } from '../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network';
import { EventDetailsPanel } from './event_details';
import { useKibana } from '../../../common/lib/kibana';
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';

jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/containers/use_search_strategy', () => ({
useSearchStrategy: jest.fn(),
}));

describe('Details Panel Component', () => {
const state: State = {
Expand Down Expand Up @@ -98,34 +99,11 @@ describe('Details Panel Component', () => {
timelineId: 'test',
};

const mockSearchStrategy = jest.fn();
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;

describe('DetailsPanel: rendering', () => {
beforeEach(() => {
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
(useKibana as jest.Mock).mockReturnValue({
services: {
data: {
search: {
searchStrategyClient: jest.fn(),
search: mockSearchStrategy.mockReturnValue({
unsubscribe: jest.fn(),
subscribe: jest.fn(),
}),
},
query: jest.fn(),
},
uiSettings: {
get: jest.fn().mockReturnValue([]),
},
application: {
navigateToApp: jest.fn(),
},
cases: {
ui: { getCasesContext: () => mockCasesContext },
},
},
});
});

test('it should not render the DetailsPanel if no expanded detail has been set in the reducer', () => {
Expand Down Expand Up @@ -266,12 +244,25 @@ describe('Details Panel Component', () => {

describe('DetailsPanel:NetworkDetails: rendering', () => {
beforeEach(() => {
mockUseSearchStrategy.mockReturnValue({
loading: true,
result: {
networkDetails: {},
},
search: jest.fn(),
refetch: jest.fn(),
inspect: {},
});
const mockState = { ...state };
mockState.timeline.timelineById[TimelineId.active].expandedDetail = networkExpandedDetail;
mockState.timeline.timelineById.test.expandedDetail = networkExpandedDetail;
store = createStore(mockState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
});

afterEach(() => {
mockUseSearchStrategy.mockReset();
});

test('it should render the Network Details view in the Details Panel when the panelView is networkDetail and the ip is set', () => {
const wrapper = mount(
<TestProviders store={store}>
Expand Down