diff --git a/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.test.ts b/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.test.ts index a211fc87fafbd..662c6db5c5358 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.test.ts @@ -9,46 +9,17 @@ import { renderHook } from '@testing-library/react-hooks'; import { Direction } from '../../../../common/search_strategy'; import type { FirstLastSeenProps } from '../../components/first_last_seen/first_last_seen'; -import { useKibana } from '../../lib/kibana'; -import { useAppToasts } from '../../hooks/use_app_toasts'; -import * as i18n from './translations'; import type { UseFirstLastSeen } from './use_first_last_seen'; import { useFirstLastSeen } from './use_first_last_seen'; -jest.mock('../../lib/kibana'); -jest.mock('../../hooks/use_app_toasts'); +import { useSearchStrategy } from '../use_search_strategy'; -const firstSeen = '2022-06-03T19:48:36.165Z'; -const lastSeen = '2022-06-13T19:48:36.165Z'; +jest.mock('../use_search_strategy', () => ({ + useSearchStrategy: jest.fn(), +})); -const mockSearchStrategy = jest.fn(); - -const mockAddError = jest.fn(); -const mockAddWarning = jest.fn(); - -(useAppToasts as jest.Mock).mockReturnValue({ - addError: mockAddError, - addWarning: mockAddWarning, -}); - -const mockKibana = (useKibana as jest.Mock).mockReturnValue({ - services: { - data: { - search: { - search: mockSearchStrategy.mockReturnValue({ - unsubscribe: jest.fn(), - subscribe: jest.fn(({ next, error }) => { - next({ firstSeen }); - return { - unsubscribe: jest.fn(), - }; - }), - }), - }, - query: jest.fn(), - }, - }, -}); +const mockUseSearchStrategy = useSearchStrategy as jest.Mock; +const mockSearch = jest.fn(); const renderUseFirstLastSeen = (overrides?: Partial) => renderHook>(() => @@ -62,10 +33,19 @@ const renderUseFirstLastSeen = (overrides?: Partial) => ); describe('useFistLastSeen', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should return default values', () => { - mockSearchStrategy.mockReturnValueOnce({ - subscribe: jest.fn(), - }); + mockUseSearchStrategy.mockImplementation(({ initialResult }) => ({ + loading: true, + result: initialResult, + search: mockSearch, + refetch: jest.fn(), + inspect: {}, + })); + const { result } = renderUseFirstLastSeen(); expect(result.current).toEqual([ @@ -73,134 +53,83 @@ describe('useFistLastSeen', () => { { errorMessage: null, firstSeen: null, - id: 'firstLastSeenQuery', lastSeen: null, - order: null, }, ]); }); it('should return parsed items for first seen', () => { + mockUseSearchStrategy.mockImplementation(() => ({ + loading: false, + result: { + firstSeen: '2022-06-03T19:48:36.165Z', + }, + search: mockSearch, + refetch: jest.fn(), + inspect: {}, + })); + const { result } = renderUseFirstLastSeen(); - expect(mockSearchStrategy).toHaveBeenCalledWith( - { - defaultIndex: [], - factoryQueryType: 'firstlastseen', - field: 'host.name', - order: 'asc', - value: 'some-host', - }, - { - abortSignal: new AbortController().signal, - strategy: 'securitySolutionSearchStrategy', - } - ); + expect(mockSearch).toHaveBeenCalledWith({ + defaultIndex: [], + factoryQueryType: 'firstlastseen', + field: 'host.name', + order: 'asc', + value: 'some-host', + }); expect(result.current).toEqual([ false, { errorMessage: null, firstSeen: '2022-06-03T19:48:36.165Z', - id: 'firstLastSeenQuery', - order: null, }, ]); }); it('should return parsed items for last seen', () => { - mockKibana.mockReturnValueOnce({ - services: { - data: { - search: { - search: mockSearchStrategy.mockReturnValue({ - unsubscribe: jest.fn(), - subscribe: jest.fn(({ next, error }) => { - next({ lastSeen }); - return { - unsubscribe: jest.fn(), - }; - }), - }), - }, - query: jest.fn(), - }, + mockUseSearchStrategy.mockImplementation(() => ({ + loading: false, + result: { + lastSeen: '2022-06-13T19:48:36.165Z', }, - }); + search: mockSearch, + refetch: jest.fn(), + inspect: {}, + })); + const { result } = renderUseFirstLastSeen({ order: Direction.desc }); - expect(mockSearchStrategy).toHaveBeenCalledWith( - { - defaultIndex: [], - factoryQueryType: 'firstlastseen', - field: 'host.name', - order: 'desc', - value: 'some-host', - }, - { - abortSignal: new AbortController().signal, - strategy: 'securitySolutionSearchStrategy', - } - ); + expect(mockSearch).toHaveBeenCalledWith({ + defaultIndex: [], + factoryQueryType: 'firstlastseen', + field: 'host.name', + order: 'desc', + value: 'some-host', + }); expect(result.current).toEqual([ false, { errorMessage: null, lastSeen: '2022-06-13T19:48:36.165Z', - id: 'firstLastSeenQuery', - order: null, }, ]); }); - it('should handle a partial, no longer running response', () => { - mockKibana.mockReturnValueOnce({ - services: { - data: { - search: { - search: mockSearchStrategy.mockReturnValue({ - unsubscribe: jest.fn(), - subscribe: jest.fn(({ next, error }) => { - next({ isRunning: false, isPartial: true }); - return { - unsubscribe: jest.fn(), - }; - }), - }), - }, - query: jest.fn(), - }, - }, - }); - - renderUseFirstLastSeen({ order: Direction.desc }); - expect(mockAddWarning).toHaveBeenCalledWith(i18n.ERROR_FIRST_LAST_SEEN_HOST); - }); it('should handle an error with search strategy', () => { const msg = 'What in tarnation!?'; - mockKibana.mockReturnValueOnce({ - services: { - data: { - search: { - search: mockSearchStrategy.mockReturnValue({ - unsubscribe: jest.fn(), - subscribe: jest.fn(({ next, error }) => { - error(msg); - return { - unsubscribe: jest.fn(), - }; - }), - }), - }, - query: jest.fn(), - }, - }, - }); + mockUseSearchStrategy.mockImplementation(() => ({ + loading: false, + result: {}, + error: new Error(msg), + search: mockSearch, + refetch: jest.fn(), + inspect: {}, + })); - renderUseFirstLastSeen({ order: Direction.desc }); - expect(mockAddError).toHaveBeenCalledWith(msg, { - title: i18n.FAIL_FIRST_LAST_SEEN_HOST, - }); + const { result } = renderUseFirstLastSeen({ order: Direction.desc }); + expect(result.current).toEqual([false, { errorMessage: `Error: ${msg}` }]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.tsx b/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.tsx index bc562edc71a34..81bbf4984c2cb 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_first_last_seen/use_first_last_seen.tsx @@ -5,30 +5,17 @@ * 2.0. */ -import deepEqual from 'fast-deep-equal'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { Subscription } from 'rxjs'; - -import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; - -import type { - Direction, - FirstLastSeenRequestOptions, - FirstLastSeenStrategyResponse, -} from '../../../../common/search_strategy'; -import { FirstLastSeenQuery } from '../../../../common/search_strategy'; -import { useAppToasts } from '../../hooks/use_app_toasts'; -import { useKibana } from '../../lib/kibana'; +import { useEffect, useMemo } from 'react'; import * as i18n from './translations'; -const ID = 'firstLastSeenQuery'; +import { useSearchStrategy } from '../use_search_strategy'; +import { FirstLastSeenQuery } from '../../../../common/search_strategy'; +import type { Direction } from '../../../../common/search_strategy'; export interface FirstLastSeenArgs { - id: string; errorMessage: string | null; firstSeen?: string | null; lastSeen?: string | null; - order: Direction.asc | Direction.desc | null; } export interface UseFirstLastSeen { field: string; @@ -43,98 +30,33 @@ export const useFirstLastSeen = ({ order, defaultIndex, }: UseFirstLastSeen): [boolean, FirstLastSeenArgs] => { - const { search } = useKibana().services.data; - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(new Subscription()); - const [loading, setLoading] = useState(false); - - const [firstLastSeenRequest, setFirstLastSeenRequest] = useState({ - defaultIndex, + const { loading, result, search, error } = useSearchStrategy({ factoryQueryType: FirstLastSeenQuery, - field, - value, - order, - }); - - const [firstLastSeenResponse, setFirstLastSeenResponse] = useState({ - order: null, - firstSeen: null, - lastSeen: null, - errorMessage: null, - id: ID, - }); - - const { addError, addWarning } = useAppToasts(); - - const firstLastSeenSearch = useCallback( - (request: FirstLastSeenRequestOptions) => { - const asyncSearch = async () => { - abortCtrl.current = new AbortController(); - setLoading(true); - searchSubscription$.current = search - .search(request, { - strategy: 'securitySolutionSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - setLoading(false); - setFirstLastSeenResponse((prevResponse) => ({ - ...prevResponse, - errorMessage: null, - firstSeen: response.firstSeen, - lastSeen: response.lastSeen, - })); - searchSubscription$.current.unsubscribe(); - } else if (isErrorResponse(response)) { - setLoading(false); - addWarning(i18n.ERROR_FIRST_LAST_SEEN_HOST); - searchSubscription$.current.unsubscribe(); - } - }, - error: (msg) => { - setLoading(false); - setFirstLastSeenResponse((prevResponse) => ({ - ...prevResponse, - errorMessage: msg, - })); - addError(msg, { - title: i18n.FAIL_FIRST_LAST_SEEN_HOST, - }); - searchSubscription$.current.unsubscribe(); - }, - }); - }; - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - asyncSearch(); + initialResult: { + firstSeen: null, + lastSeen: null, }, - [search, addError, addWarning] - ); + errorMessage: i18n.FAIL_FIRST_LAST_SEEN_HOST, + }); useEffect(() => { - setFirstLastSeenRequest((prevRequest) => { - const myRequest = { - ...prevRequest, - defaultIndex, - field, - value, - }; - if (!deepEqual(prevRequest, myRequest)) { - return myRequest; - } - return prevRequest; + search({ + defaultIndex, + factoryQueryType: FirstLastSeenQuery, + field, + value, + order, }); - }, [defaultIndex, field, value]); - - useEffect(() => { - firstLastSeenSearch(firstLastSeenRequest); - return () => { - searchSubscription$.current.unsubscribe(); - abortCtrl.current.abort(); - }; - }, [firstLastSeenRequest, firstLastSeenSearch]); + }, [defaultIndex, field, value, order, search]); + + const setFirstLastSeenResponse: FirstLastSeenArgs = useMemo( + () => ({ + firstSeen: result.firstSeen, + lastSeen: result.lastSeen, + errorMessage: error ? (error as Error).toString() : null, + }), + [result, error] + ); - return [loading, firstLastSeenResponse]; + return [loading, setFirstLastSeenResponse]; };