Skip to content

Commit

Permalink
Refactoring host overview to use useSearchStrategy (#135860)
Browse files Browse the repository at this point in the history
* useSearchStrategy

* update unit tests

* fix unit tests

* update unit tests

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
angorayc and kibanamachine authored Jul 21, 2022
1 parent a9780d2 commit 8985518
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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, useHostDetails } 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 = {
endDate: '2020-07-08T08:20:18.966Z',
hostName: 'my-macbook',
id: ID,
indexNames: ['fakebeat-*'],
skip: false,
startDate: '2020-07-07T08:20:18.966Z',
};

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

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

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

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

expect(mockSearch).not.toHaveBeenCalled();
});
it('skip = true will cancel any running request', () => {
const props = {
...defaultProps,
};
const { rerender } = renderHook(() => useHostDetails(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,15 @@
* 2.0.
*/

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

import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../common/store';
import { useKibana } from '../../../../common/lib/kibana';
import type {
HostItem,
HostDetailsRequestOptions,
HostDetailsStrategyResponse,
} from '../../../../../common/search_strategy/security_solution/hosts';
import type { HostItem } from '../../../../../common/search_strategy/security_solution/hosts';
import { HostsQueries } from '../../../../../common/search_strategy/security_solution/hosts';

import * as i18n from './translations';
import { getInspectResponse } from '../../../../helpers';
import type { InspectResponse } from '../../../../types';
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';

export const ID = 'hostsDetailsQuery';

Expand Down Expand Up @@ -53,104 +43,53 @@ export const useHostDetails = ({
skip = false,
startDate,
}: UseHostDetails): [boolean, HostDetailsArgs] => {
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 [hostDetailsRequest, setHostDetailsRequest] = useState<HostDetailsRequestOptions | null>(
null
);
const { addError, addWarning } = useAppToasts();

const [hostDetailsResponse, setHostDetailsResponse] = useState<HostDetailsArgs>({
endDate,
hostDetails: {},
id,
inspect: {
dsl: [],
response: [],
const {
loading,
result: response,
search,
refetch,
inspect,
} = useSearchStrategy<HostsQueries.details>({
factoryQueryType: HostsQueries.details,
initialResult: {
hostDetails: {},
},
refetch: refetch.current,
startDate,
errorMessage: i18n.FAIL_HOST_OVERVIEW,
abort: skip,
});

const hostDetailsSearch = useCallback(
(request: HostDetailsRequestOptions | null) => {
if (request == null || skip) {
return;
}

const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);

searchSubscription$.current = data.search
.search<HostDetailsRequestOptions, HostDetailsStrategyResponse>(request, {
strategy: 'securitySolutionSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
setLoading(false);
setHostDetailsResponse((prevResponse) => ({
...prevResponse,
hostDetails: response.hostDetails,
inspect: getInspectResponse(response, prevResponse.inspect),
refetch: refetch.current,
}));
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading(false);
addWarning(i18n.ERROR_HOST_OVERVIEW);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_HOST_OVERVIEW,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, addError, addWarning, skip]
const hostDetailsResponse = useMemo(
() => ({
endDate,
hostDetails: response.hostDetails,
id,
inspect,
isInspected: false,
refetch,
startDate,
}),
[endDate, response.hostDetails, id, inspect, refetch, startDate]
);

useEffect(() => {
setHostDetailsRequest((prevRequest) => {
const myRequest = {
...(prevRequest ?? {}),
defaultIndex: indexNames,
factoryQueryType: HostsQueries.details,
hostName,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [endDate, hostName, indexNames, startDate]);
const hostDetailsRequest = useMemo(
() => ({
defaultIndex: indexNames,
factoryQueryType: HostsQueries.details,
hostName,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
}),
[endDate, hostName, indexNames, startDate]
);

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

return [loading, hostDetailsResponse];
};
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 @@ -247,12 +225,28 @@ describe('Details Panel Component', () => {

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

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

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

0 comments on commit 8985518

Please sign in to comment.