Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions src/plugins/es_ui_shared/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
UseRequestResponse,
sendRequest,
useRequest,
ResponseInterceptor,
} from './request';

export { indices } from './indices';
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/es_ui_shared/public/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@
* Side Public License, v 1.
*/

export { SendRequestConfig, SendRequestResponse, sendRequest } from './send_request';
export {
SendRequestConfig,
SendRequestResponse,
sendRequest,
ResponseInterceptor,
} from './send_request';
export { UseRequestConfig, UseRequestResponse, useRequest } from './use_request';
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import {

export interface SendRequestHelpers {
getSendRequestSpy: () => sinon.SinonStub;
sendSuccessRequest: () => Promise<SendRequestResponse>;
sendSuccessRequest: (
responseInterceptors?: SendRequestConfig['responseInterceptors']
) => Promise<SendRequestResponse>;
getSuccessResponse: () => SendRequestResponse;
sendErrorRequest: () => Promise<SendRequestResponse>;
sendErrorRequest: (
responseInterceptors?: SendRequestConfig['responseInterceptors']
) => Promise<SendRequestResponse>;
getErrorResponse: () => SendRequestResponse;
}

Expand Down Expand Up @@ -49,7 +53,8 @@ export const createSendRequestHelpers = (): SendRequestHelpers => {
})
)
.resolves(successResponse);
const sendSuccessRequest = () => sendRequest({ ...successRequest });
const sendSuccessRequest = (responseInterceptors?: SendRequestConfig['responseInterceptors']) =>
sendRequest({ ...successRequest, responseInterceptors });
const getSuccessResponse = () => ({ data: successResponse.data, error: null });

// Set up failed request helpers.
Expand All @@ -62,7 +67,8 @@ export const createSendRequestHelpers = (): SendRequestHelpers => {
})
)
.rejects(errorResponse);
const sendErrorRequest = () => sendRequest({ ...errorRequest });
const sendErrorRequest = (responseInterceptors?: SendRequestConfig['responseInterceptors']) =>
sendRequest({ ...errorRequest, responseInterceptors });
const getErrorResponse = () => ({
data: null,
error: errorResponse.response.data,
Expand Down
25 changes: 23 additions & 2 deletions src/plugins/es_ui_shared/public/request/send_request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,29 @@ describe('sendRequest function', () => {
const { sendErrorRequest, getSendRequestSpy, getErrorResponse } = helpers;

// For some reason sinon isn't throwing an error on rejection, as an awaited Promise normally would.
const error = await sendErrorRequest();
const errorResponse = await sendErrorRequest();
sinon.assert.calledOnce(getSendRequestSpy());
expect(error).toEqual(getErrorResponse());
expect(errorResponse).toEqual(getErrorResponse());
});

it('calls responseInterceptors with successful responses', async () => {
const { sendSuccessRequest, getSuccessResponse } = helpers;
const successInterceptorSpy = sinon.spy();
const successInterceptors = [successInterceptorSpy];

await sendSuccessRequest(successInterceptors);
sinon.assert.calledOnce(successInterceptorSpy);
sinon.assert.calledWith(successInterceptorSpy, getSuccessResponse());
});

it('calls responseInterceptors with errors', async () => {
const { sendErrorRequest, getErrorResponse } = helpers;
const errorInterceptorSpy = sinon.spy();
const errorInterceptors = [errorInterceptorSpy];

// For some reason sinon isn't throwing an error on rejection, as an awaited Promise normally would.
await sendErrorRequest(errorInterceptors);
sinon.assert.calledOnce(errorInterceptorSpy);
sinon.assert.calledWith(errorInterceptorSpy, getErrorResponse());
});
});
28 changes: 23 additions & 5 deletions src/plugins/es_ui_shared/public/request/send_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public';

export type ResponseInterceptor = ({ data, error }: { data: any; error: any }) => void;

export interface SendRequestConfig {
path: string;
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
Expand All @@ -18,33 +20,49 @@ export interface SendRequestConfig {
* HttpFetchOptions#asSystemRequest.
*/
asSystemRequest?: boolean;
responseInterceptors?: ResponseInterceptor[];
}

export interface SendRequestResponse<D = any, E = any> {
data: D | null;
error: E | null;
}

// Pass the response sequentially through each interceptor, allowing for
// side effects to be run.
const updateResponseInterceptors = (
response: any,
responseInterceptors: ResponseInterceptor[] = []
) => {
responseInterceptors.forEach((interceptor) => interceptor(response));
};

export const sendRequest = async <D = any, E = any>(
httpClient: HttpSetup,
{ path, method, body, query, asSystemRequest }: SendRequestConfig
{ path, method, body, query, asSystemRequest, responseInterceptors }: SendRequestConfig
): Promise<SendRequestResponse<D, E>> => {
try {
const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body);
const response = await httpClient[method](path, {
const rawResponse = await httpClient[method](path, {
body: stringifiedBody,
query,
asSystemRequest,
});

return {
data: response.data ? response.data : response,
const response = {
data: rawResponse.data ? rawResponse.data : rawResponse,
error: null,
};

updateResponseInterceptors(response, responseInterceptors);
return response;
} catch (e) {
return {
const response = {
data: null,
error: e.response?.data ?? e.body,
};

updateResponseInterceptors(response, responseInterceptors);
return response;
}
};
15 changes: 12 additions & 3 deletions src/plugins/es_ui_shared/public/request/use_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ export interface UseRequestResponse<D = any, E = Error> {

export const useRequest = <D = any, E = Error>(
httpClient: HttpSetup,
{ path, method, query, body, pollIntervalMs, initialData, deserializer }: UseRequestConfig
{
path,
method,
query,
body,
pollIntervalMs,
initialData,
deserializer,
responseInterceptors,
}: UseRequestConfig
): UseRequestResponse<D, E> => {
const isMounted = useRef(false);

Expand Down Expand Up @@ -80,7 +89,7 @@ export const useRequest = <D = any, E = Error>(
// Any requests that are sent in the background (without user interaction) should be flagged as "system requests". This should not be
// confused with any terminology in Elasticsearch. This is a Kibana-specific construct that allows the server to differentiate between
// user-initiated and requests "system"-initiated requests, for purposes like security features.
const requestPayload = { ...requestBody, asSystemRequest };
const requestPayload = { ...requestBody, asSystemRequest, responseInterceptors };
const response = await sendRequest<D, E>(httpClient, requestPayload);
const { data: serializedResponseData, error: responseError } = response;

Expand All @@ -106,7 +115,7 @@ export const useRequest = <D = any, E = Error>(
// Setting isLoading to false also acts as a signal for scheduling the next poll request.
setIsLoading(false);
},
[requestBody, httpClient, deserializer, clearPollInterval]
[requestBody, httpClient, deserializer, clearPollInterval, responseInterceptors]
);

const scheduleRequest = useCallback(() => {
Expand Down