mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Extract useRequest and sendRequest from SR into es-ui-shared plugin (#40777)
- Refactor SR's consumption to send UI metric telemetry in the promise chain, to uncouple the request functions from telemetry. - Refactor useRequest to return isInitialRequest instead of tracking polling state. - Rename request callback to sendRequest. - Rename loading flag to isLoading. - Rename changeInterval callback to setRequestInterval. - Rename interval parameter to pollIntervalMs. - Remove initialData from return value. - Preserve complete response shape instead of removing everything except the data property. - Send a request when path, body, or method changes. - Don't send a request when the interval changes. - Remove setRequestInterval from return value. - Expect the consumer to change the interval config in order to set it to a new value. - Refactor internals so that calling sendRequest resets the interval. - Migrate Watcher to use shared request library. - Rename onSuccess to deserializer and use it to process data. - Rename updateInterval function to scheduleRequest. - Don’t watch method parameter.
This commit is contained in:
parent
55ba53ce1d
commit
c91e94510e
20 changed files with 682 additions and 439 deletions
26
src/plugins/es_ui_shared/public/request/index.ts
Normal file
26
src/plugins/es_ui_shared/public/request/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
SendRequestConfig,
|
||||
SendRequestResponse,
|
||||
UseRequestConfig,
|
||||
sendRequest,
|
||||
useRequest,
|
||||
} from './request';
|
247
src/plugins/es_ui_shared/public/request/request.test.js
Normal file
247
src/plugins/es_ui_shared/public/request/request.test.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
sendRequest as sendRequestUnbound,
|
||||
useRequest as useRequestUnbound,
|
||||
} from './request';
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
const TestHook = ({ callback }) => {
|
||||
callback();
|
||||
return null;
|
||||
};
|
||||
|
||||
let element;
|
||||
|
||||
const testHook = (callback) => {
|
||||
element = mount(<TestHook callback={callback} />);
|
||||
};
|
||||
|
||||
const wait = async wait =>
|
||||
new Promise(resolve => setTimeout(resolve, wait || 1));
|
||||
|
||||
describe('request lib', () => {
|
||||
const successRequest = { path: '/success', method: 'post', body: {} };
|
||||
const errorRequest = { path: '/error', method: 'post', body: {} };
|
||||
const successResponse = { statusCode: 200, data: { message: 'Success message' } };
|
||||
const errorResponse = { statusCode: 400, statusText: 'Error message' };
|
||||
|
||||
let sendPost;
|
||||
let sendRequest;
|
||||
let useRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
sendPost = sinon.stub();
|
||||
sendPost.withArgs(successRequest.path, successRequest.body).returns(successResponse);
|
||||
sendPost.withArgs(errorRequest.path, errorRequest.body).throws(errorResponse);
|
||||
|
||||
const httpClient = {
|
||||
post: (...args) => {
|
||||
return sendPost(...args);
|
||||
},
|
||||
};
|
||||
|
||||
sendRequest = sendRequestUnbound.bind(null, httpClient);
|
||||
useRequest = useRequestUnbound.bind(null, httpClient);
|
||||
});
|
||||
|
||||
describe('sendRequest function', () => {
|
||||
it('uses the provided path, method, and body to send the request', async () => {
|
||||
const response = await sendRequest({ ...successRequest });
|
||||
sinon.assert.calledOnce(sendPost);
|
||||
expect(response).toEqual({ data: successResponse.data });
|
||||
});
|
||||
|
||||
it('surfaces errors', async () => {
|
||||
try {
|
||||
await sendRequest({ ...errorRequest });
|
||||
} catch(e) {
|
||||
sinon.assert.calledOnce(sendPost);
|
||||
expect(e).toBe(errorResponse.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRequest hook', () => {
|
||||
let hook;
|
||||
|
||||
function initUseRequest(config) {
|
||||
act(() => {
|
||||
testHook(() => {
|
||||
hook = useRequest(config);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('parameters', () => {
|
||||
describe('path, method, body', () => {
|
||||
it('is used to send the request', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
await wait(10);
|
||||
expect(hook.data).toBe(successResponse.data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollIntervalMs', () => {
|
||||
it('sends another request after the specified time has elapsed', async () => {
|
||||
initUseRequest({ ...successRequest, pollIntervalMs: 30 });
|
||||
await wait(5);
|
||||
sinon.assert.calledOnce(sendPost);
|
||||
|
||||
await wait(40);
|
||||
sinon.assert.calledTwice(sendPost);
|
||||
|
||||
// We have to manually clean up or else the interval will continue to fire requests,
|
||||
// interfering with other tests.
|
||||
element.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialData', () => {
|
||||
it('sets the initial data value', () => {
|
||||
initUseRequest({ ...successRequest, initialData: 'initialData' });
|
||||
expect(hook.data).toBe('initialData');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deserializer', () => {
|
||||
it('is called once the request resolves', async () => {
|
||||
const deserializer = sinon.stub();
|
||||
initUseRequest({ ...successRequest, deserializer });
|
||||
sinon.assert.notCalled(deserializer);
|
||||
|
||||
await wait(5);
|
||||
sinon.assert.calledOnce(deserializer);
|
||||
sinon.assert.calledWith(deserializer, successResponse.data);
|
||||
});
|
||||
|
||||
it('processes data', async () => {
|
||||
initUseRequest({ ...successRequest, deserializer: () => 'intercepted' });
|
||||
await wait(5);
|
||||
expect(hook.data).toBe('intercepted');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', () => {
|
||||
describe('isInitialRequest', () => {
|
||||
it('is true for the first request and false for subsequent requests', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
expect(hook.isInitialRequest).toBe(true);
|
||||
|
||||
hook.sendRequest();
|
||||
await wait(5);
|
||||
expect(hook.isInitialRequest).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLoading', () => {
|
||||
it('represents in-flight request status', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
expect(hook.isLoading).toBe(true);
|
||||
|
||||
await wait(5);
|
||||
expect(hook.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
it('surfaces errors from requests', async () => {
|
||||
initUseRequest({ ...errorRequest });
|
||||
await wait(10);
|
||||
expect(hook.error).toBe(errorResponse);
|
||||
});
|
||||
|
||||
it('persists while a request is in-flight', async () => {
|
||||
initUseRequest({ ...errorRequest });
|
||||
await wait(5);
|
||||
hook.sendRequest();
|
||||
expect(hook.isLoading).toBe(true);
|
||||
expect(hook.error).toBe(errorResponse);
|
||||
});
|
||||
|
||||
it('is undefined when the request is successful', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
await wait(10);
|
||||
expect(hook.isLoading).toBe(false);
|
||||
expect(hook.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('surfaces payloads from requests', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
await wait(10);
|
||||
expect(hook.data).toBe(successResponse.data);
|
||||
});
|
||||
|
||||
it('persists while a request is in-flight', async () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
await wait(5);
|
||||
hook.sendRequest();
|
||||
expect(hook.isLoading).toBe(true);
|
||||
expect(hook.data).toBe(successResponse.data);
|
||||
});
|
||||
|
||||
it('is undefined when the request fails', async () => {
|
||||
initUseRequest({ ...errorRequest });
|
||||
await wait(10);
|
||||
expect(hook.isLoading).toBe(false);
|
||||
expect(hook.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
describe('sendRequest', () => {
|
||||
it('sends the request', () => {
|
||||
initUseRequest({ ...successRequest });
|
||||
sinon.assert.calledOnce(sendPost);
|
||||
hook.sendRequest();
|
||||
sinon.assert.calledTwice(sendPost);
|
||||
});
|
||||
|
||||
it('resets the pollIntervalMs', async () => {
|
||||
initUseRequest({ ...successRequest, pollIntervalMs: 30 });
|
||||
await wait(5);
|
||||
sinon.assert.calledOnce(sendPost);
|
||||
|
||||
await wait(20);
|
||||
hook.sendRequest();
|
||||
|
||||
// If the request didn't reset the interval, there would have been three requests sent by now.
|
||||
await wait(20);
|
||||
sinon.assert.calledTwice(sendPost);
|
||||
|
||||
await wait(20);
|
||||
sinon.assert.calledThrice(sendPost);
|
||||
|
||||
// We have to manually clean up or else the interval will continue to fire requests,
|
||||
// interfering with other tests.
|
||||
element.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
154
src/plugins/es_ui_shared/public/request/request.ts
Normal file
154
src/plugins/es_ui_shared/public/request/request.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
export interface SendRequestConfig {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
export interface SendRequestResponse {
|
||||
data: any;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export interface UseRequestConfig extends SendRequestConfig {
|
||||
pollIntervalMs?: number;
|
||||
initialData?: any;
|
||||
deserializer?: (data: any) => any;
|
||||
}
|
||||
|
||||
export const sendRequest = async (
|
||||
httpClient: ng.IHttpService,
|
||||
{ path, method, body }: SendRequestConfig
|
||||
): Promise<Partial<SendRequestResponse>> => {
|
||||
try {
|
||||
const response = await (httpClient as any)[method](path, body);
|
||||
|
||||
if (typeof response.data === 'undefined') {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return { data: response.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e.response ? e.response : e,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const useRequest = (
|
||||
httpClient: ng.IHttpService,
|
||||
{
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
pollIntervalMs,
|
||||
initialData,
|
||||
deserializer = (data: any): any => data,
|
||||
}: UseRequestConfig
|
||||
) => {
|
||||
// Main states for tracking request status and data
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// Consumers can use isInitialRequest to implement a polling UX.
|
||||
const [isInitialRequest, setIsInitialRequest] = useState<boolean>(true);
|
||||
const pollInterval = useRef<any>(null);
|
||||
const pollIntervalId = useRef<any>(null);
|
||||
|
||||
// We always want to use the most recently-set interval in scheduleRequest.
|
||||
pollInterval.current = pollIntervalMs;
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const scheduleRequest = () => {
|
||||
// Clear current interval
|
||||
if (pollIntervalId.current) {
|
||||
clearTimeout(pollIntervalId.current);
|
||||
}
|
||||
|
||||
// Set new interval
|
||||
if (pollInterval.current) {
|
||||
pollIntervalId.current = setTimeout(_sendRequest, pollInterval.current);
|
||||
}
|
||||
};
|
||||
|
||||
const _sendRequest = async () => {
|
||||
// We don't clear error or data, so it's up to the consumer to decide whether to display the
|
||||
// "old" error/data or loading state when a new request is in-flight.
|
||||
setIsLoading(true);
|
||||
|
||||
const requestBody = {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
};
|
||||
|
||||
const response = await sendRequest(httpClient, requestBody);
|
||||
const { data: serializedResponseData, error: responseError } = response;
|
||||
const responseData = deserializer(serializedResponseData);
|
||||
|
||||
// If an outdated request has resolved, DON'T update state, but DO allow the processData handler
|
||||
// to execute side effects like update telemetry.
|
||||
if (isOutdatedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(responseError);
|
||||
setData(responseData);
|
||||
setIsLoading(false);
|
||||
setIsInitialRequest(false);
|
||||
|
||||
// If we're on an interval, we need to schedule the next request. This also allows us to reset
|
||||
// the interval if the user has manually requested the data, to avoid doubled-up requests.
|
||||
scheduleRequest();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_sendRequest();
|
||||
// To be functionally correct we'd send a new request if the method, path, or body changes.
|
||||
// But it doesn't seem likely that the method will change and body is likely to be a new
|
||||
// object even if its shape hasn't changed, so for now we're just watching the path.
|
||||
}, [path]);
|
||||
|
||||
useEffect(() => {
|
||||
scheduleRequest();
|
||||
|
||||
// Clean up intervals and inflight requests and corresponding state changes
|
||||
return () => {
|
||||
isOutdatedRequest = true;
|
||||
if (pollIntervalId.current) {
|
||||
clearTimeout(pollIntervalId.current);
|
||||
}
|
||||
};
|
||||
}, [pollIntervalMs]);
|
||||
|
||||
return {
|
||||
isInitialRequest,
|
||||
isLoading,
|
||||
error,
|
||||
data,
|
||||
sendRequest: _sendRequest, // Gives the user the ability to manually request data
|
||||
};
|
||||
};
|
|
@ -54,7 +54,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
// Load repository types
|
||||
const {
|
||||
error: repositoryTypesError,
|
||||
loading: repositoryTypesLoading,
|
||||
isLoading: repositoryTypesLoading,
|
||||
data: repositoryTypes = [],
|
||||
} = useLoadRepositoryTypes();
|
||||
|
||||
|
|
|
@ -45,14 +45,14 @@ interface Props {
|
|||
}
|
||||
|
||||
export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => {
|
||||
const { loading, error, data: privilegesData } = useRequest({
|
||||
const { isLoading, error, data: privilegesData } = useRequest({
|
||||
path: privilegesEndpoint,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const value = {
|
||||
isLoading: loading,
|
||||
privileges: loading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData,
|
||||
isLoading,
|
||||
privileges: isLoading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData,
|
||||
apiError: error ? error : null,
|
||||
};
|
||||
|
||||
|
|
|
@ -36,11 +36,11 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
|
|||
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
isLoading,
|
||||
data: { policies } = {
|
||||
policies: undefined,
|
||||
},
|
||||
request: reload,
|
||||
sendRequest: reload,
|
||||
} = useLoadPolicies();
|
||||
|
||||
const openPolicyDetailsUrl = (newPolicyName: SlmPolicy['name']): string => {
|
||||
|
@ -61,7 +61,7 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
|
|||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -36,12 +36,12 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
isLoading,
|
||||
data: { repositories, managedRepository } = {
|
||||
repositories: undefined,
|
||||
managedRepository: undefined,
|
||||
},
|
||||
request: reload,
|
||||
sendRequest: reload,
|
||||
} = useLoadRepositories();
|
||||
|
||||
const openRepositoryDetailsUrl = (newRepositoryName: Repository['name']): string => {
|
||||
|
@ -71,7 +71,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -50,7 +50,7 @@ export const RestoreList: React.FunctionComponent = () => {
|
|||
const [currentInterval, setCurrentInterval] = useState<number>(INTERVAL_OPTIONS[1]);
|
||||
|
||||
// Load restores
|
||||
const { error, loading, data: restores = [], polling, changeInterval } = useLoadRestores(
|
||||
const { error, isLoading, data: restores = [], isInitialRequest, sendRequest } = useLoadRestores(
|
||||
currentInterval
|
||||
);
|
||||
|
||||
|
@ -62,139 +62,148 @@ export const RestoreList: React.FunctionComponent = () => {
|
|||
|
||||
let content: JSX.Element;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.loadingRestoresDescription"
|
||||
defaultMessage="Loading restores…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
if (isInitialRequest) {
|
||||
if (isLoading) {
|
||||
// Because we're polling for new data, we only want to hide the list during the initial fetch.
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.LoadingRestoresErrorMessage"
|
||||
defaultMessage="Error loading restores"
|
||||
id="xpack.snapshotRestore.restoreList.loadingRestoresDescription"
|
||||
defaultMessage="Loading restores…"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (restores && restores.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
// If we get an error while polling we don't need to show it to the user because they can still
|
||||
// work with the table.
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptTitle"
|
||||
defaultMessage="You don't have any restored snapshots"
|
||||
id="xpack.snapshotRestore.restoreList.loadingRestoresErrorMessage"
|
||||
defaultMessage="Error loading restores"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptDescription"
|
||||
defaultMessage="Go to {snapshotsLink} to start a restore."
|
||||
values={{
|
||||
snapshotsLink: (
|
||||
<EuiLink href={linkToSnapshots()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptDescriptionLink"
|
||||
defaultMessage="Snapshots"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
content = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="srRestoreListIntervalMenu"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
type="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setIsIntervalMenuOpen(!isIntervalMenuOpen)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenuButtonText"
|
||||
defaultMessage="Refresh data every {interval}"
|
||||
values={{
|
||||
interval:
|
||||
currentInterval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(currentInterval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(currentInterval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isIntervalMenuOpen}
|
||||
closePopover={() => setIsIntervalMenuOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={INTERVAL_OPTIONS.map(interval => (
|
||||
<EuiContextMenuItem
|
||||
key={interval}
|
||||
icon="empty"
|
||||
onClick={() => {
|
||||
changeInterval(interval);
|
||||
setCurrentInterval(interval);
|
||||
setIsIntervalMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{interval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(interval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(interval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
))}
|
||||
if (restores && restores.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptTitle"
|
||||
defaultMessage="You don't have any restored snapshots"
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{polling ? <EuiLoadingSpinner size="m" /> : null}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<RestoreTable restores={restores || []} />
|
||||
</Fragment>
|
||||
);
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptDescription"
|
||||
defaultMessage="Go to {snapshotsLink} to start a restore."
|
||||
values={{
|
||||
snapshotsLink: (
|
||||
<EuiLink href={linkToSnapshots()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptDescriptionLink"
|
||||
defaultMessage="Snapshots"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="srRestoreListIntervalMenu"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
type="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setIsIntervalMenuOpen(!isIntervalMenuOpen)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenuButtonText"
|
||||
defaultMessage="Refresh data every {interval}"
|
||||
values={{
|
||||
interval:
|
||||
currentInterval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(currentInterval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(currentInterval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isIntervalMenuOpen}
|
||||
closePopover={() => setIsIntervalMenuOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={INTERVAL_OPTIONS.map(interval => (
|
||||
<EuiContextMenuItem
|
||||
key={interval}
|
||||
icon="empty"
|
||||
onClick={() => {
|
||||
sendRequest();
|
||||
setCurrentInterval(interval);
|
||||
setIsIntervalMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{interval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(interval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(interval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
))}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isLoading ? <EuiLoadingSpinner size="m" /> : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<RestoreTable restores={restores || []} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -41,9 +41,9 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
isLoading,
|
||||
data: { snapshots = [], repositories = [], errors = {} },
|
||||
request: reload,
|
||||
sendRequest: reload,
|
||||
} = useLoadSnapshots();
|
||||
|
||||
const openSnapshotDetailsUrl = (
|
||||
|
@ -104,7 +104,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -46,7 +46,7 @@ export const RepositoryEdit: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
// Load repository
|
||||
const {
|
||||
error: repositoryError,
|
||||
loading: loadingRepository,
|
||||
isLoading: loadingRepository,
|
||||
data: repositoryData,
|
||||
} = useLoadRepository(name);
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export const RestoreSnapshot: React.FunctionComponent<RouteComponentProps<MatchP
|
|||
const [snapshotDetails, setSnapshotDetails] = useState<SnapshotDetails | {}>({});
|
||||
|
||||
// Load snapshot
|
||||
const { error: snapshotError, loading: loadingSnapshot, data: snapshotData } = useLoadSnapshot(
|
||||
const { error: snapshotError, isLoading: loadingSnapshot, data: snapshotData } = useLoadSnapshot(
|
||||
repositoryName,
|
||||
snapshotId
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
UIM_REPOSITORY_DELETE_MANY,
|
||||
UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
|
||||
} from '../../constants';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
|
@ -30,14 +31,17 @@ export const useLoadRepository = (name: Repository['name']) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const verifyRepository = (name: Repository['name']) => {
|
||||
return sendRequest({
|
||||
export const verifyRepository = async (name: Repository['name']) => {
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${encodeURIComponent(name)}/verify`
|
||||
),
|
||||
method: 'get',
|
||||
uimActionType: UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_REPOSITORY_DETAIL_PANEL_VERIFY);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useLoadRepositoryTypes = () => {
|
||||
|
@ -49,31 +53,40 @@ export const useLoadRepositoryTypes = () => {
|
|||
};
|
||||
|
||||
export const addRepository = async (newRepository: Repository | EmptyRepository) => {
|
||||
return sendRequest({
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
|
||||
method: 'put',
|
||||
body: newRepository,
|
||||
uimActionType: UIM_REPOSITORY_CREATE,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_REPOSITORY_CREATE);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const editRepository = async (editedRepository: Repository | EmptyRepository) => {
|
||||
return sendRequest({
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${encodeURIComponent(editedRepository.name)}`
|
||||
),
|
||||
method: 'put',
|
||||
body: editedRepository,
|
||||
uimActionType: UIM_REPOSITORY_UPDATE,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_REPOSITORY_UPDATE);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const deleteRepositories = async (names: Array<Repository['name']>) => {
|
||||
return sendRequest({
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${names.map(name => encodeURIComponent(name)).join(',')}`
|
||||
),
|
||||
method: 'delete',
|
||||
uimActionType: names.length > 1 ? UIM_REPOSITORY_DELETE_MANY : UIM_REPOSITORY_DELETE,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(names.length > 1 ? UIM_REPOSITORY_DELETE_MANY : UIM_REPOSITORY_DELETE);
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { RestoreSettings } from '../../../../common/types';
|
||||
import { UIM_RESTORE_CREATE } from '../../constants';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
|
@ -14,21 +15,24 @@ export const executeRestore = async (
|
|||
snapshot: string,
|
||||
restoreSettings: RestoreSettings
|
||||
) => {
|
||||
return sendRequest({
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}restore/${encodeURIComponent(repository)}/${encodeURIComponent(snapshot)}`
|
||||
),
|
||||
method: 'post',
|
||||
body: restoreSettings,
|
||||
uimActionType: UIM_RESTORE_CREATE,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_RESTORE_CREATE);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useLoadRestores = (interval?: number) => {
|
||||
export const useLoadRestores = (pollIntervalMs?: number) => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}restores`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
interval,
|
||||
pollIntervalMs,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { UIM_SNAPSHOT_DELETE, UIM_SNAPSHOT_DELETE_MANY } from '../../constants';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
|
@ -28,13 +29,16 @@ export const useLoadSnapshot = (repositoryName: string, snapshotId: string) =>
|
|||
export const deleteSnapshots = async (
|
||||
snapshotIds: Array<{ snapshot: string; repository: string }>
|
||||
) => {
|
||||
return sendRequest({
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}snapshots/${snapshotIds
|
||||
.map(({ snapshot, repository }) => encodeURIComponent(`${repository}/${snapshot}`))
|
||||
.join(',')}`
|
||||
),
|
||||
method: 'delete',
|
||||
uimActionType: snapshotIds.length > 1 ? UIM_SNAPSHOT_DELETE_MANY : UIM_SNAPSHOT_DELETE,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(snapshotIds.length > 1 ? UIM_SNAPSHOT_DELETE_MANY : UIM_SNAPSHOT_DELETE);
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -3,160 +3,20 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import {
|
||||
SendRequestConfig,
|
||||
SendRequestResponse,
|
||||
UseRequestConfig,
|
||||
sendRequest as _sendRequest,
|
||||
useRequest as _useRequest,
|
||||
} from '../../../shared_imports';
|
||||
import { httpService } from './index';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
|
||||
interface SendRequest {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: any;
|
||||
uimActionType?: string;
|
||||
}
|
||||
|
||||
interface SendRequestResponse {
|
||||
data: any;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
|
||||
export const sendRequest = async ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
uimActionType,
|
||||
}: SendRequest): Promise<Partial<SendRequestResponse>> => {
|
||||
try {
|
||||
const response = await httpService.httpClient[method](path, body);
|
||||
|
||||
if (typeof response.data === 'undefined') {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// Track successful request
|
||||
if (uimActionType) {
|
||||
trackUiMetric(uimActionType);
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e.response ? e.response : e,
|
||||
};
|
||||
}
|
||||
export const sendRequest = (config: SendRequestConfig): Promise<Partial<SendRequestResponse>> => {
|
||||
return _sendRequest(httpService.httpClient, config);
|
||||
};
|
||||
|
||||
interface UseRequest extends SendRequest {
|
||||
interval?: number;
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
export const useRequest = ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
interval,
|
||||
initialData,
|
||||
uimActionType,
|
||||
}: UseRequest) => {
|
||||
// Main states for tracking request status and data
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// States for tracking polling
|
||||
const [polling, setPolling] = useState<boolean>(false);
|
||||
const [currentInterval, setCurrentInterval] = useState<UseRequest['interval']>(interval);
|
||||
const intervalRequest = useRef<any>(null);
|
||||
const isFirstRequest = useRef<boolean>(true);
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const request = async () => {
|
||||
const isPollRequest = currentInterval && !isFirstRequest.current;
|
||||
|
||||
// Don't reset main error/loading states if we are doing polling
|
||||
if (isPollRequest) {
|
||||
setPolling(true);
|
||||
} else {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setLoading(true);
|
||||
setPolling(false);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
uimActionType,
|
||||
};
|
||||
|
||||
const response = await sendRequest(requestBody);
|
||||
|
||||
// Don't update state if an outdated request has resolved.
|
||||
if (isOutdatedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set just data if we are doing polling
|
||||
if (isPollRequest) {
|
||||
setPolling(false);
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
}
|
||||
} else {
|
||||
setError(response.error);
|
||||
setData(response.data);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
isFirstRequest.current = false;
|
||||
};
|
||||
|
||||
const cancelOutdatedRequest = () => {
|
||||
isOutdatedRequest = true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Perform request
|
||||
request();
|
||||
|
||||
// Clear current interval
|
||||
if (intervalRequest.current) {
|
||||
clearInterval(intervalRequest.current);
|
||||
}
|
||||
|
||||
// Set new interval
|
||||
if (currentInterval) {
|
||||
intervalRequest.current = setInterval(request, currentInterval);
|
||||
}
|
||||
|
||||
// Cleanup intervals and inflight requests and corresponding state changes
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
if (intervalRequest.current) {
|
||||
clearInterval(intervalRequest.current);
|
||||
}
|
||||
};
|
||||
}, [path, currentInterval]);
|
||||
|
||||
return {
|
||||
error,
|
||||
loading,
|
||||
data,
|
||||
request,
|
||||
polling,
|
||||
changeInterval: (newInterval: UseRequest['interval']) => {
|
||||
// Allow changing polling interval if there was one set
|
||||
if (!interval) {
|
||||
return;
|
||||
}
|
||||
setCurrentInterval(newInterval);
|
||||
},
|
||||
};
|
||||
export const useRequest = (config: UseRequestConfig) => {
|
||||
return _useRequest(httpService.httpClient, config);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 {
|
||||
SendRequestConfig,
|
||||
SendRequestResponse,
|
||||
UseRequestConfig,
|
||||
sendRequest,
|
||||
useRequest,
|
||||
} from '../../../../../src/plugins/es_ui_shared/public/request';
|
|
@ -36,12 +36,12 @@ export const getSavedObjectsClient = () => {
|
|||
|
||||
const basePath = chrome.addBasePath(ROUTES.API_ROOT);
|
||||
|
||||
export const loadWatches = (interval: number) => {
|
||||
export const loadWatches = (pollIntervalMs: number) => {
|
||||
return useRequest({
|
||||
path: `${basePath}/watches`,
|
||||
method: 'get',
|
||||
interval,
|
||||
processData: ({ watches = [] }: { watches: any[] }) => {
|
||||
pollIntervalMs,
|
||||
deserializer: ({ watches = [] }: { watches: any[] }) => {
|
||||
return watches.map((watch: any) => Watch.fromUpstreamJson(watch));
|
||||
},
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ export const loadWatchDetail = (id: string) => {
|
|||
return useRequest({
|
||||
path: `${basePath}/watch/${id}`,
|
||||
method: 'get',
|
||||
processData: ({ watch = {} }: { watch: any }) => Watch.fromUpstreamJson(watch),
|
||||
deserializer: ({ watch = {} }: { watch: any }) => Watch.fromUpstreamJson(watch),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -65,7 +65,7 @@ export const loadWatchHistory = (id: string, startTime: string) => {
|
|||
return useRequest({
|
||||
path,
|
||||
method: 'get',
|
||||
processData: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => {
|
||||
deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => {
|
||||
return watchHistoryItems.map((historyItem: any) =>
|
||||
WatchHistoryItem.fromUpstreamJson(historyItem)
|
||||
);
|
||||
|
@ -75,9 +75,9 @@ export const loadWatchHistory = (id: string, startTime: string) => {
|
|||
|
||||
export const loadWatchHistoryDetail = (id: string | undefined) => {
|
||||
return useRequest({
|
||||
path: !id ? undefined : `${basePath}/history/${id}`,
|
||||
path: !id ? '' : `${basePath}/history/${id}`,
|
||||
method: 'get',
|
||||
processData: ({ watchHistoryItem }: { watchHistoryItem: any }) =>
|
||||
deserializer: ({ watchHistoryItem }: { watchHistoryItem: any }) =>
|
||||
WatchHistoryItem.fromUpstreamJson(watchHistoryItem),
|
||||
});
|
||||
};
|
||||
|
@ -164,7 +164,7 @@ export const getWatchVisualizationData = (watchModel: BaseWatch, visualizeOption
|
|||
watch: watchModel.upstreamJson,
|
||||
options: visualizeOptions.upstreamJson,
|
||||
},
|
||||
processData: ({ visualizeData }: { visualizeData: any }) => visualizeData,
|
||||
deserializer: ({ visualizeData }: { visualizeData: any }) => visualizeData,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -172,7 +172,7 @@ export const loadSettings = () => {
|
|||
return useRequest({
|
||||
path: `${basePath}/settings`,
|
||||
method: 'get',
|
||||
processData: (data: {
|
||||
deserializer: (data: {
|
||||
action_types: {
|
||||
[key: string]: {
|
||||
enabled: boolean;
|
||||
|
|
|
@ -4,120 +4,19 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
SendRequestConfig,
|
||||
SendRequestResponse,
|
||||
UseRequestConfig,
|
||||
sendRequest as _sendRequest,
|
||||
useRequest as _useRequest,
|
||||
} from '../shared_imports';
|
||||
import { getHttpClient } from './api';
|
||||
|
||||
interface SendRequest {
|
||||
path?: string;
|
||||
method: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
interface SendRequestResponse {
|
||||
data: any;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export const sendRequest = async ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
}: SendRequest): Promise<Partial<SendRequestResponse>> => {
|
||||
try {
|
||||
const response = await (getHttpClient() as any)[method](path, body);
|
||||
|
||||
if (typeof response.data === 'undefined') {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e.response ? e.response : e,
|
||||
};
|
||||
}
|
||||
export const sendRequest = (config: SendRequestConfig): Promise<Partial<SendRequestResponse>> => {
|
||||
return _sendRequest(getHttpClient(), config);
|
||||
};
|
||||
|
||||
interface UseRequest extends SendRequest {
|
||||
interval?: number;
|
||||
initialData?: any;
|
||||
processData?: any;
|
||||
}
|
||||
|
||||
export const useRequest = ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
interval,
|
||||
initialData,
|
||||
processData,
|
||||
}: UseRequest) => {
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const createRequest = async (isInitialRequest = true) => {
|
||||
// Set a neutral state for a non-request.
|
||||
if (!path) {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
// Only set loading state to true and initial data on the first request
|
||||
if (isInitialRequest) {
|
||||
setIsLoading(true);
|
||||
setData(initialData);
|
||||
}
|
||||
|
||||
const { data: responseData, error: responseError } = await sendRequest({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
});
|
||||
|
||||
// Don't update state if an outdated request has resolved.
|
||||
if (isOutdatedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(responseError);
|
||||
setData(processData && responseData ? processData(responseData) : responseData);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function cancelOutdatedRequest() {
|
||||
isOutdatedRequest = true;
|
||||
}
|
||||
|
||||
createRequest();
|
||||
|
||||
if (interval) {
|
||||
const intervalRequest = setInterval(createRequest.bind(null, false), interval);
|
||||
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
clearInterval(intervalRequest);
|
||||
};
|
||||
}
|
||||
|
||||
// Called when a new render will trigger this effect.
|
||||
return cancelOutdatedRequest;
|
||||
}, [path]);
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
data,
|
||||
createRequest,
|
||||
};
|
||||
export const useRequest = (config: UseRequestConfig) => {
|
||||
return _useRequest(getHttpClient(), config);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useContext, useEffect, useState } from 'react';
|
||||
import React, { Fragment, useContext, useEffect } from 'react';
|
||||
import {
|
||||
AnnotationDomainTypes,
|
||||
Axis,
|
||||
|
@ -130,21 +130,21 @@ export const WatchVisualization = () => {
|
|||
// Fetching visualization data is independent of watch actions
|
||||
const watchWithoutActions = new ThresholdWatch({ ...watch, actions: [] });
|
||||
|
||||
const [isInitialRequest, setIsInitialRequest] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
isInitialRequest,
|
||||
isLoading,
|
||||
data: watchVisualizationData,
|
||||
error,
|
||||
createRequest: reload,
|
||||
sendRequest: reload,
|
||||
} = getWatchVisualizationData(watchWithoutActions, visualizeOptions);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevents refetch on initial render
|
||||
// Prevent sending a second request on initial render.
|
||||
if (isInitialRequest) {
|
||||
return setIsInitialRequest(false);
|
||||
return;
|
||||
}
|
||||
reload(false);
|
||||
|
||||
reload();
|
||||
}, [
|
||||
index,
|
||||
timeField,
|
||||
|
@ -161,7 +161,7 @@ export const WatchVisualization = () => {
|
|||
threshold,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
if (isInitialRequest && isLoading) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingChart size="xl" />}
|
||||
|
@ -283,5 +283,6 @@ export const WatchVisualization = () => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
13
x-pack/legacy/plugins/watcher/public/shared_imports.ts
Normal file
13
x-pack/legacy/plugins/watcher/public/shared_imports.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 {
|
||||
SendRequestConfig,
|
||||
SendRequestResponse,
|
||||
UseRequestConfig,
|
||||
sendRequest,
|
||||
useRequest,
|
||||
} from '../../../../../src/plugins/es_ui_shared/public/request';
|
Loading…
Add table
Add a link
Reference in a new issue