Skip to content

Commit

Permalink
Split utility hooks into several files
Browse files Browse the repository at this point in the history
  • Loading branch information
weltenwort committed Dec 10, 2020
1 parent 8f62d8c commit acf0ceb
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 242 deletions.
7 changes: 2 additions & 5 deletions x-pack/plugins/infra/public/containers/logs/log_entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import {
logEntrySearchResponsePayloadRT,
LOG_ENTRY_SEARCH_STRATEGY,
} from '../../../common/search_strategies/log_entries/log_entry';
import {
useDataSearch,
useLatestPartialDataSearchRequest,
} from '../../utils/use_data_search_request';
import { useDataSearch, useLatestPartialDataSearchResponse } from '../../utils/data_search';

export const useLogEntry = ({
sourceId,
Expand Down Expand Up @@ -44,7 +41,7 @@ export const useLogEntry = ({
latestResponseErrors,
loaded,
total,
} = useLatestPartialDataSearchRequest(
} = useLatestPartialDataSearchResponse(
logEntrySearchRequests$,
null,
decodeLogEntrySearchResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ won't be executed unless a subscriber subscribes to it. And in order to cleanly
cancel and garbage collect the subscription it should be integrated with the
React component life-cycle.

The `useLatestPartialDataSearchRequest()` does that in such a way that the
The `useLatestPartialDataSearchResponse()` does that in such a way that the
newest response observable is subscribed to and that any previous response
observables are unsubscribed from for proper cancellation if a new request has
been created. This uses RxJS's `switchMap()` operator under the hood. The hook
Expand All @@ -75,7 +75,7 @@ an appropriate way.
A request can fail due to various reasons that include servers-side errors,
Elasticsearch shard failures and network failures. The intention is to map all
of them to a common `SearchStrategyError` interface. While the
`useLatestPartialDataSearchRequest()` hook does that for errors emitted
`useLatestPartialDataSearchResponse()` hook does that for errors emitted
natively by the response `Observable`, it's the responsibility of the
projection function to handle errors that are encoded in the response body,
which includes most server-side errors. Note that errors and partial results in
Expand All @@ -94,7 +94,7 @@ const {
latestResponseErrors,
loaded,
total,
} = useLatestPartialDataSearchRequest(
} = useLatestPartialDataSearchResponse(
requests$,
'initialValue',
useMemo(() => decodeOrThrow(mySearchStrategyResponsePayloadRT), []),
Expand All @@ -104,7 +104,7 @@ const {
## Representing the request state to the user

After the values have been made available to the React rendering process using
the `useLatestPartialDataSearchRequest()` hook, normal component hierarchies
the `useLatestPartialDataSearchResponse()` hook, normal component hierarchies
can be used to make the request state and result available to the user. The
following utility components can make that even easier.

Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/infra/public/utils/data_search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export * from './types';
export * from './use_data_search_request';
export * from './use_latest_partial_data_search_response';
36 changes: 36 additions & 0 deletions x-pack/plugins/infra/public/utils/data_search/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Observable } from 'rxjs';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
} from '../../../../../../src/plugins/data/public';
import { SearchStrategyError } from '../../../common/search_strategies/common/errors';

export interface DataSearchRequestDescriptor<Request extends IKibanaSearchRequest, RawResponse> {
request: Request;
options: ISearchOptions;
response$: Observable<IKibanaSearchResponse<RawResponse>>;
abortController: AbortController;
}

export interface NormalizedKibanaSearchResponse<ResponseData> {
total?: number;
loaded?: number;
isRunning: boolean;
isPartial: boolean;
data: ResponseData;
errors: SearchStrategyError[];
}

export interface DataSearchResponseDescriptor<Request extends IKibanaSearchRequest, Response> {
request: Request;
options: ISearchOptions;
response: NormalizedKibanaSearchResponse<Response>;
abortController: AbortController;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ import { Observable, of, Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import {
DataPublicPluginStart,
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchGeneric,
ISearchStart,
} from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public';
import { PluginKibanaContextValue } from '../hooks/use_kibana';
import {
DataSearchRequestDescriptor,
useDataSearch,
useLatestPartialDataSearchRequest,
} from './use_data_search_request';
} from '../../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { createKibanaReactContext } from '../../../../../../src/plugins/kibana_react/public';
import { PluginKibanaContextValue } from '../../hooks/use_kibana';
import { useDataSearch } from './use_data_search_request';

describe('useDataSearch hook', () => {
it('forwards the search function arguments to the getRequest function', async () => {
Expand Down Expand Up @@ -185,108 +180,6 @@ describe('useDataSearch hook', () => {
});
});

describe('useLatestPartialDataSearchRequest hook', () => {
it("subscribes to the latest request's response observable", () => {
const firstRequest = {
abortController: new AbortController(),
options: {},
request: { params: 'firstRequestParam' },
response$: new Subject<IKibanaSearchResponse<string>>(),
};

const secondRequest = {
abortController: new AbortController(),
options: {},
request: { params: 'secondRequestParam' },
response$: new Subject<IKibanaSearchResponse<string>>(),
};

const requests$ = new Subject<
DataSearchRequestDescriptor<IKibanaSearchRequest<string>, string>
>();

const { result } = renderHook(() =>
useLatestPartialDataSearchRequest(requests$, 'initial', (response) => ({
data: `projection of ${response}`,
}))
);

expect(result).toHaveProperty('current.isRequestRunning', false);
expect(result).toHaveProperty('current.latestResponseData', undefined);

// first request is started
act(() => {
requests$.next(firstRequest);
});

expect(result).toHaveProperty('current.isRequestRunning', true);
expect(result).toHaveProperty('current.latestResponseData', 'initial');

// first response of the first request arrives
act(() => {
firstRequest.response$.next({ rawResponse: 'request-1-response-1', isRunning: true });
});

expect(result).toHaveProperty('current.isRequestRunning', true);
expect(result).toHaveProperty(
'current.latestResponseData',
'projection of request-1-response-1'
);

// second request is started before the second response of the first request arrives
act(() => {
requests$.next(secondRequest);
secondRequest.response$.next({ rawResponse: 'request-2-response-1', isRunning: true });
});

expect(result).toHaveProperty('current.isRequestRunning', true);
expect(result).toHaveProperty(
'current.latestResponseData',
'projection of request-2-response-1'
);

// second response of the second request arrives
act(() => {
secondRequest.response$.next({ rawResponse: 'request-2-response-2', isRunning: false });
});

expect(result).toHaveProperty('current.isRequestRunning', false);
expect(result).toHaveProperty(
'current.latestResponseData',
'projection of request-2-response-2'
);
});

it("unsubscribes from the latest request's response observable on unmount", () => {
const onUnsubscribe = jest.fn();

const firstRequest = {
abortController: new AbortController(),
options: {},
request: { params: 'firstRequestParam' },
response$: new Observable<IKibanaSearchResponse<string>>(() => {
return onUnsubscribe;
}),
};

const requests$ = of<DataSearchRequestDescriptor<IKibanaSearchRequest<string>, string>>(
firstRequest
);

const { unmount } = renderHook(() =>
useLatestPartialDataSearchRequest(requests$, 'initial', (response) => ({
data: `projection of ${response}`,
}))
);

expect(onUnsubscribe).not.toHaveBeenCalled();

unmount();

expect(onUnsubscribe).toHaveBeenCalled();
});
});

const createDataPluginMock = () => {
const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & {
search: ISearchStart & { search: jest.MockedFunction<ISearchGeneric> };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useCallback } from 'react';
import { Observable, of, Subject } from 'rxjs';
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
} from '../../../../../../src/plugins/data/public';
import { AbortError } from '../../../../../../src/plugins/kibana_utils/public';
import { SearchStrategyError } from '../../../common/search_strategies/common/errors';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { tapUnsubscribe, useLatest, useObservable, useObservableState } from '../use_observable';

export type DataSearchRequestFactory<Args extends any[], Request extends IKibanaSearchRequest> = (
...args: Args
) =>
| {
request: Request;
options: ISearchOptions;
}
| null
| undefined;

export const useDataSearch = <
RequestFactoryArgs extends any[],
Request extends IKibanaSearchRequest,
RawResponse
>({
getRequest,
}: {
getRequest: DataSearchRequestFactory<RequestFactoryArgs, Request>;
}) => {
const { services } = useKibanaContextForPlugin();
const request$ = useObservable(
() => new Subject<{ request: Request; options: ISearchOptions }>(),
[]
);
const requests$ = useObservable(
(inputs$) =>
inputs$.pipe(
switchMap(([currentRequest$]) => currentRequest$),
map(({ request, options }) => {
const abortController = new AbortController();
let isAbortable = true;

return {
abortController,
request,
options,
response$: services.data.search
.search<Request, IKibanaSearchResponse<RawResponse>>(request, {
abortSignal: abortController.signal,
...options,
})
.pipe(
// avoid aborting failed or completed requests
tap({
error: () => {
isAbortable = false;
},
complete: () => {
isAbortable = false;
},
}),
tapUnsubscribe(() => {
if (isAbortable) {
abortController.abort();
}
}),
share()
),
};
})
),
[request$]
);

const search = useCallback(
(...args: RequestFactoryArgs) => {
const request = getRequest(...args);

if (request) {
request$.next(request);
}
},
[getRequest, request$]
);

return {
requests$,
search,
};
};
Loading

0 comments on commit acf0ceb

Please sign in to comment.